diff --git a/application/pom.xml b/application/pom.xml index e3dcd9f0f9..dc9f6a6bf1 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard application @@ -85,6 +85,10 @@ org.thingsboard.common.transport coap + + org.thingsboard.common.transport + lwm2m + org.thingsboard.common.transport snmp @@ -279,7 +283,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf index c504076bb0..0ce4295535 100644 --- a/application/src/main/conf/thingsboard.conf +++ b/application/src/main/conf/thingsboard.conf @@ -15,11 +15,10 @@ # export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@ -Dinstall.data_dir=@pkg.installFolder@/data" -export JAVA_OPTS="$JAVA_OPTS -Xloggc:@pkg.logFolder@/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=@pkg.logFolder@/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" export LOG_FILENAME=${pkg.name}.out export LOADER_PATH=${pkg.installFolder}/conf,${pkg.installFolder}/extensions export SQL_DATA_FOLDER=${pkg.installFolder}/data/sql diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 0d97afd5e7..437d56678e 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -47,7 +47,7 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"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', amount = 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}\"},\"_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;\"},{\"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;\"}]}],\"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\"}" @@ -134,4 +134,4 @@ } } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 1010013689..140004c37c 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -25,22 +25,6 @@ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars - Chart.js\"}" } }, - { - "alias": "basic_timeseries", - "name": "Timeseries - Flot", - "descriptor": { - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "resources": [], - "templateHtml": "", - "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", - "settingsSchema": "{}", - "dataKeySettingsSchema": "{}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"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;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"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\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" - } - }, { "alias": "doughnut_chart_js", "name": "Doughnut - Chart.js", @@ -71,7 +55,7 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n", "settingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" @@ -138,8 +122,8 @@ } }, { - "alias": "timeseries_bars_flot", - "name": "Timeseries Bars - Flot", + "alias": "state_chart", + "name": "State Chart", "descriptor": { "type": "timeseries", "sizeX": 8, @@ -147,15 +131,15 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" } }, { - "alias": "state_chart", - "name": "State Chart", + "alias": "basic_timeseries", + "name": "Timeseries - Flot", "descriptor": { "type": "timeseries", "sizeX": 8, @@ -163,11 +147,27 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", "settingsSchema": "{}", "dataKeySettingsSchema": "{}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"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;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"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\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}" + } + }, + { + "alias": "timeseries_bars_flot", + "name": "Timeseries Bars - Flot", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}" } } ] -} +} \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/navigation_widgets.json b/application/src/main/data/json/system/widget_bundles/navigation_widgets.json new file mode 100644 index 0000000000..c37e21906c --- /dev/null +++ b/application/src/main/data/json/system/widget_bundles/navigation_widgets.json @@ -0,0 +1,41 @@ +{ + "widgetsBundle": { + "alias": "navigation_widgets", + "title": "Navigation widgets", + "image": null + }, + "widgetTypes": [ + { + "alias": "navigation_cards", + "name": "Navigation cards", + "descriptor": { + "type": "static", + "sizeX": 7, + "sizeY": 6, + "resources": [], + "templateHtml": "", + "templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"filterType\": {\n \"title\": \"Filter type\",\n \"type\": \"string\",\n \"default\": \"all\"\n },\n \"filter\": {\n \"title\": \"Items\",\n \"type\": \"array\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"filterType\",\n \"type\": \"radios\",\n \"direction\": \"row\",\n \"titleMap\": [\n {\n \"value\": \"all\",\n \"name\": \"All items\"\n },\n {\n \"value\": \"include\",\n \"name\": \"Include items\"\n },\n {\n \"value\": \"exclude\",\n \"name\": \"Exclude items\"\n }\n ]\n },\n {\n \"key\": \"filter\",\n \"type\": \"rc-select\",\n \"condition\": \"model.filterType !== 'all'\",\n \"tags\": true,\n \"placeholder\": \"Enter urls to filter\",\n \"items\": [{\"value\": \"/devices\", \"label\": \"/devices\"}, {\"value\": \"/assets\", \"label\": \"/assets\"}, {\"value\": \"/deviceProfies\", \"label\": \"/deviceProfies\"}]\n }\n ]\n}\n", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + } + }, + { + "alias": "navigation_card", + "name": "Navigation card", + "descriptor": { + "type": "static", + "sizeX": 2.5, + "sizeY": 2, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"name\": {\n \"title\": \"Title\",\n \"type\": \"string\",\n \"default\": \"{i18n:device.devices}\"\n },\n \"icon\": {\n \"title\": \"icon\",\n \"type\": \"string\",\n \"default\": \"devices_other\"\n },\n \"path\": {\n \"title\": \"Navigation path\",\n \"type\": \"string\",\n \"default\": \"/devices\"\n }\n },\n \"required\": [\"name\", \"icon\", \"path\"]\n },\n \"form\": [\n \"name\",\n {\n \"key\": \"icon\",\n \"type\": \"icon\"\n },\n \"path\"\n ]\n}\n", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + } + } + ] +} \ No newline at end of file diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql index a650244d5a..3c2d43e197 100644 --- a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql +++ b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql @@ -84,11 +84,12 @@ BEGIN END IF; END IF; END IF; - END IF; - IF partition_to_delete IS NOT NULL THEN - RAISE NOTICE 'Partition to delete by max ttl: %', partition_to_delete; - EXECUTE format('DROP TABLE %I', partition_to_delete); - deleted := deleted + 1; + IF partition_to_delete IS NOT NULL THEN + RAISE NOTICE 'Partition to delete by max ttl: %', partition_to_delete; + EXECUTE format('DROP TABLE IF EXISTS %I', partition_to_delete); + partition_to_delete := NULL; + deleted := deleted + 1; + END IF; END IF; END LOOP; END IF; diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java index 6eaac0157b..32c995ac82 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java @@ -30,7 +30,8 @@ import java.util.Arrays; "org.thingsboard.server.service.component", "org.thingsboard.server.service.install", "org.thingsboard.server.dao", - "org.thingsboard.server.common.stats"}) + "org.thingsboard.server.common.stats", + "org.thingsboard.server.cache"}) public class ThingsboardInstallApplication { private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; 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 0a12e93171..f1248a3aad 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 @@ -134,12 +134,12 @@ public class AppActor extends ContextAwareActor { private void onQueueToRuleEngineMsg(QueueToRuleEngineMsg msg) { if (TenantId.SYS_TENANT_ID.equals(msg.getTenantId())) { - msg.getTbMsg().getCallback().onFailure(new RuleEngineException("Message has system tenant id!")); + msg.getMsg().getCallback().onFailure(new RuleEngineException("Message has system tenant id!")); } else { if (!deletedTenants.contains(msg.getTenantId())) { getOrCreateTenantActor(msg.getTenantId()).tell(msg); } else { - msg.getTbMsg().getCallback().onSuccess(); + msg.getMsg().getCallback().onSuccess(); } } } diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java index 00f47c7383..9233f4a317 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java @@ -62,7 +62,7 @@ public class DeviceActor extends ContextAwareActor { processor.processAttributesUpdate(ctx, (DeviceAttributesEventNotificationMsg) msg); break; case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG: - processor.processCredentialsUpdate(); + processor.processCredentialsUpdate(msg); break; case DEVICE_NAME_OR_TYPE_UPDATE_TO_DEVICE_ACTOR_MSG: processor.processNameOrTypeUpdate((DeviceNameOrTypeUpdateMsg) msg); 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 263323559d..41073939f2 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 @@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.thingsboard.rule.engine.api.RpcError; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; +import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorCtx; @@ -36,6 +37,9 @@ import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; @@ -61,6 +65,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseM import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportToDeviceActorMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; import org.thingsboard.server.service.rpc.FromDeviceRpcResponse; import org.thingsboard.server.service.rpc.ToDeviceRpcRequestActorMsg; @@ -450,11 +455,19 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { dumpSessions(); } - void processCredentialsUpdate() { - sessions.forEach(this::notifyTransportAboutClosedSession); - attributeSubscriptions.clear(); - rpcSubscriptions.clear(); - dumpSessions(); + void processCredentialsUpdate(TbActorMsg msg) { + if (((DeviceCredentialsUpdateNotificationMsg) msg).getDeviceCredentials().getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) { + log.info("1) LwM2Mtype: "); + sessions.forEach((k, v) -> { + notifyTransportAboutProfileUpdate(k, v, ((DeviceCredentialsUpdateNotificationMsg) msg).getDeviceCredentials()); + }); + } else { + sessions.forEach(this::notifyTransportAboutClosedSession); + attributeSubscriptions.clear(); + rpcSubscriptions.clear(); + dumpSessions(); + + } } private void notifyTransportAboutClosedSession(UUID sessionId, SessionInfoMetaData sessionMd) { @@ -465,6 +478,18 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { systemContext.getTbCoreToTransportService().process(sessionMd.getSessionInfo().getNodeId(), msg); } + void notifyTransportAboutProfileUpdate(UUID sessionId, SessionInfoMetaData sessionMd, DeviceCredentials deviceCredentials) { + log.info("2) LwM2Mtype: "); + TransportProtos.ToTransportUpdateCredentialsProto.Builder notification = TransportProtos.ToTransportUpdateCredentialsProto.newBuilder(); + notification.addCredentialsId(deviceCredentials.getCredentialsId()); + notification.addCredentialsValue(deviceCredentials.getCredentialsValue()); + ToTransportMsg msg = ToTransportMsg.newBuilder() + .setSessionIdMSB(sessionId.getMostSignificantBits()) + .setSessionIdLSB(sessionId.getLeastSignificantBits()) + .setToTransportUpdateCredentialsNotification(notification).build(); + systemContext.getTbCoreToTransportService().process(sessionMd.getSessionInfo().getNodeId(), msg); + } + void processNameOrTypeUpdate(DeviceNameOrTypeUpdateMsg msg) { this.deviceName = msg.getDeviceName(); this.deviceType = msg.getDeviceType(); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index c608c4a8a8..e1717e3b9b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.channel.EventLoopGroup; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.StringUtils; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; @@ -34,11 +33,11 @@ import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.sms.SmsSenderFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActorRef; -import org.thingsboard.server.common.data.ApiUsageRecordKey; 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.EntityType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -90,10 +89,12 @@ class DefaultTbContext implements TbContext { public final static ObjectMapper mapper = new ObjectMapper(); private final ActorSystemContext mainCtx; + private final String ruleChainName; private final RuleNodeCtx nodeCtx; - public DefaultTbContext(ActorSystemContext mainCtx, RuleNodeCtx nodeCtx) { + public DefaultTbContext(ActorSystemContext mainCtx, String ruleChainName, RuleNodeCtx nodeCtx) { this.mainCtx = mainCtx; + this.ruleChainName = ruleChainName; this.nodeCtx = nodeCtx; } @@ -117,13 +118,13 @@ class DefaultTbContext implements TbContext { relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th)); } msg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId()); - nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationTypes, msg, th != null ? th.getMessage() : null)); + nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId(), relationTypes, msg, th != null ? th.getMessage() : null)); } @Override public void tellSelf(TbMsg msg, long delayMs) { //TODO: add persistence layer - scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor()); + scheduleMsgWithDelay(new RuleNodeToSelfMsg(this, msg), delayMs, nodeCtx.getSelfActor()); } @Override @@ -254,7 +255,8 @@ class DefaultTbContext implements TbContext { } else { failureMessage = null; } - nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), + nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), + nodeCtx.getSelf().getId(), Collections.singleton(TbRelationTypes.FAILURE), msg, failureMessage)); } @@ -277,7 +279,21 @@ class DefaultTbContext implements TbContext { } public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) { - return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED); + RuleChainId ruleChainId = null; + String queueName = ServiceQueue.MAIN; + if (device.getDeviceProfileId() != null) { + DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId()); + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", device.getDeviceProfileId()); + ruleChainId = null; + queueName = ServiceQueue.MAIN; + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + String defaultQueueName = deviceProfile.getDefaultQueueName(); + queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + } + } + return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, queueName, ruleChainId); } public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) { @@ -285,12 +301,31 @@ class DefaultTbContext implements TbContext { } public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) { - return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action); + RuleChainId ruleChainId = null; + String queueName = ServiceQueue.MAIN; + if (EntityType.DEVICE.equals(alarm.getOriginator().getEntityType())) { + DeviceId deviceId = new DeviceId(alarm.getOriginator().getId()); + DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId); + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", deviceId); + ruleChainId = null; + queueName = ServiceQueue.MAIN; + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + String defaultQueueName = deviceProfile.getDefaultQueueName(); + queueName = defaultQueueName != null ? defaultQueueName : ServiceQueue.MAIN; + } + } + return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action, queueName, ruleChainId); } public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) { + return entityActionMsg(entity, id, ruleNodeId, action, ServiceQueue.MAIN, null); + } + + public TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, String queueName, RuleChainId ruleChainId) { try { - return TbMsg.newMsg(action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity))); + return TbMsg.newMsg(queueName, action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), ruleChainId, null); } catch (JsonProcessingException | IllegalArgumentException e) { throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e); } @@ -301,6 +336,16 @@ class DefaultTbContext implements TbContext { return nodeCtx.getSelf().getId(); } + @Override + public RuleNode getSelf() { + return nodeCtx.getSelf(); + } + + @Override + public String getRuleChainName() { + return ruleChainName; + } + @Override public TenantId getTenantId() { return nodeCtx.getTenantId(); @@ -475,11 +520,6 @@ class DefaultTbContext implements TbContext { return mainCtx.getCassandraBufferedRateExecutor().submit(task); } - @Override - public RedisTemplate getRedisTemplate() { - return mainCtx.getRedisTemplate(); - } - @Override public PageData findRuleNodeStates(PageLink pageLink) { if (log.isDebugEnabled()) { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 579fae829e..830714b431 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -23,7 +23,6 @@ import org.thingsboard.server.actors.TbActorRef; import org.thingsboard.server.actors.TbEntityActorId; import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.shared.ComponentMsgProcessor; -import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; @@ -36,6 +35,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.common.msg.plugin.RuleNodeUpdatedMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; @@ -132,7 +132,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor componentClazz = Class.forName(ruleNode.getType()); - tbNode = (TbNode) (componentClazz.newInstance()); + tbNode = (TbNode) (componentClazz.getDeclaredConstructor().newInstance()); tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration())); } return tbNode; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java index 32e1a7536b..f7a26403aa 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java @@ -15,11 +15,16 @@ */ package org.thingsboard.server.actors.ruleChain; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbRuleEngineActorMsg; +import org.thingsboard.server.common.msg.queue.RuleEngineException; import java.io.Serializable; import java.util.Set; @@ -27,15 +32,34 @@ import java.util.Set; /** * Created by ashvayka on 19.03.18. */ -@Data -class RuleNodeToRuleChainTellNextMsg implements TbActorMsg, Serializable { +@EqualsAndHashCode(callSuper = true) +@ToString +class RuleNodeToRuleChainTellNextMsg extends TbRuleEngineActorMsg implements Serializable { private static final long serialVersionUID = 4577026446412871820L; + @Getter + private final RuleChainId ruleChainId; + @Getter private final RuleNodeId originator; + @Getter private final Set relationTypes; - private final TbMsg msg; + @Getter private final String failureMessage; + public RuleNodeToRuleChainTellNextMsg(RuleChainId ruleChainId, RuleNodeId originator, Set relationTypes, TbMsg tbMsg, String failureMessage) { + super(tbMsg); + this.ruleChainId = ruleChainId; + this.originator = originator; + this.relationTypes = relationTypes; + this.failureMessage = failureMessage; + } + + @Override + public void onTbActorStopped(TbActorStopReason reason) { + String message = reason == TbActorStopReason.STOPPED ? String.format("Rule chain [%s] stopped", ruleChainId.getId()) : String.format("Failed to initialize rule chain [%s]!", ruleChainId.getId()); + msg.getCallback().onFailure(new RuleEngineException(message)); + } + @Override public MsgType getMsgType() { return MsgType.RULE_TO_RULE_CHAIN_TELL_NEXT_MSG; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java index bdd33f2381..3ebaa7ffa8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java @@ -15,18 +15,25 @@ */ package org.thingsboard.server.actors.ruleChain; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbRuleEngineActorMsg; +import org.thingsboard.server.common.msg.queue.RuleNodeException; /** * Created by ashvayka on 19.03.18. */ -@Data -final class RuleNodeToSelfMsg implements TbActorMsg { +@EqualsAndHashCode(callSuper = true) +@ToString +final class RuleNodeToSelfMsg extends TbToRuleNodeActorMsg { - private final TbMsg msg; + public RuleNodeToSelfMsg(TbContext ctx, TbMsg tbMsg) { + super(ctx, tbMsg); + } @Override public MsgType getMsgType() { diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/TbToRuleNodeActorMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/TbToRuleNodeActorMsg.java new file mode 100644 index 0000000000..6d492f630a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/TbToRuleNodeActorMsg.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 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.ruleChain; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.msg.TbActorStopReason; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbRuleEngineActorMsg; +import org.thingsboard.server.common.msg.queue.RuleNodeException; + +@EqualsAndHashCode(callSuper = true) +public abstract class TbToRuleNodeActorMsg extends TbRuleEngineActorMsg { + + @Getter + private final TbContext ctx; + + public TbToRuleNodeActorMsg(TbContext ctx, TbMsg tbMsg) { + super(tbMsg); + this.ctx = ctx; + } + + @Override + public void onTbActorStopped(TbActorStopReason reason) { + String message = reason == TbActorStopReason.STOPPED ? "Rule node stopped" : "Failed to initialize rule node!"; + msg.getCallback().onFailure(new RuleNodeException(message, ctx.getRuleChainName(), ctx.getSelf())); + } +} diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java index 05dc056715..e356209532 100644 --- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java @@ -20,6 +20,7 @@ import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.TbActor; import org.thingsboard.server.actors.TbActorCtx; import org.thingsboard.server.actors.TbActorException; +import org.thingsboard.server.actors.TbRuleNodeUpdateException; import org.thingsboard.server.actors.shared.ComponentMsgProcessor; import org.thingsboard.server.actors.stats.StatsPersistMsg; import org.thingsboard.server.common.data.id.EntityId; @@ -123,6 +124,9 @@ public abstract class ComponentActor implements ActorService { public static final String APP_DISPATCHER_NAME = "app-dispatcher"; public static final String TENANT_DISPATCHER_NAME = "tenant-dispatcher"; @@ -120,10 +121,10 @@ public class DefaultActorService implements ActorService { appActor.tellWithHighPriority(new AppInitMsg()); } - @EventListener(PartitionChangeEvent.class) - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + @Override + protected void onTbApplicationEvent(PartitionChangeEvent event) { log.info("Received partition change event."); - this.appActor.tellWithHighPriority(new PartitionChangeMsg(partitionChangeEvent.getServiceQueueKey(), partitionChangeEvent.getPartitions())); + this.appActor.tellWithHighPriority(new PartitionChangeMsg(event.getServiceQueueKey(), event.getPartitions())); } @PreDestroy diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java index 5e6f69278d..1f64c71629 100644 --- a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java @@ -28,10 +28,10 @@ import org.thingsboard.server.common.msg.TbActorMsg; @ToString public final class StatsPersistMsg implements TbActorMsg { - private long messagesProcessed; - private long errorsOccurred; - private TenantId tenantId; - private EntityId entityId; + private final long messagesProcessed; + private final long errorsOccurred; + private final TenantId tenantId; + private final EntityId entityId; @Override public MsgType getMsgType() { diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java index f8edccf692..012e26e216 100644 --- a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java +++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java @@ -18,7 +18,7 @@ package org.thingsboard.server.actors.stats; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; -public final class StatsPersistTick implements TbActorMsg{ +public final class StatsPersistTick implements TbActorMsg { @Override public MsgType getMsgType() { return MsgType.STATS_PERSIST_TICK_MSG; 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 acd583e795..d6d1d6aa9a 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 @@ -119,7 +119,7 @@ public class TenantActor extends RuleChainManagerActor { log.info("[{}] Processing missing Tenant msg: {}", tenantId, msg); if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) { QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg; - queueMsg.getTbMsg().getCallback().onSuccess(); + queueMsg.getMsg().getCallback().onSuccess(); } else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)) { TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg; transportMsg.getCallback().onSuccess(); @@ -177,7 +177,7 @@ public class TenantActor extends RuleChainManagerActor { log.warn("RECEIVED INVALID MESSAGE: {}", msg); return; } - TbMsg tbMsg = msg.getTbMsg(); + TbMsg tbMsg = msg.getMsg(); if (apiUsageState.isReExecEnabled()) { if (tbMsg.getRuleChainId() == null) { if (getRootChainActor() != null) { diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index 0fcaf90525..bbcb7d656a 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -91,6 +91,7 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza return action; } + @SuppressWarnings("deprecation") private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) { if (registrationId == null) { return null; diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index a012bd7766..8b595bd987 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -48,6 +48,7 @@ import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticati import org.thingsboard.server.service.security.auth.jwt.RefreshTokenProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.SkipPathRequestMatcher; import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor; +import org.thingsboard.server.service.security.auth.oauth2.HttpCookieOAuth2AuthorizationRequestRepository; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider; import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter; import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; @@ -84,6 +85,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @Qualifier("oauth2AuthenticationFailureHandler") private AuthenticationFailureHandler oauth2AuthenticationFailureHandler; + @Autowired(required = false) + private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; + @Autowired @Qualifier("defaultAuthenticationSuccessHandler") private AuthenticationSuccessHandler successHandler; @@ -127,7 +131,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt } protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception { - List pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); + List pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT)); SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); @@ -213,7 +217,9 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); if (oauth2Configuration != null) { http.oauth2Login() - .authorizationEndpoint().authorizationRequestResolver(oAuth2AuthorizationRequestResolver) + .authorizationEndpoint() + .authorizationRequestRepository(httpCookieOAuth2AuthorizationRequestRepository) + .authorizationRequestResolver(oAuth2AuthorizationRequestResolver) .and() .loginPage("/oauth2Login") .loginProcessingUrl(oauth2Configuration.getLoginProcessingUrl()) diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index 1bda7dfab7..3f77da93f7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -215,6 +215,7 @@ public class AuthController extends BaseController { User user = userService.findUserById(TenantId.SYS_TENANT_ID, credentials.getUserId()); UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); + userService.setUserCredentialsEnabled(user.getTenantId(), user.getId(), true); String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); String loginUrl = String.format("%s/login", baseUrl); String email = user.getEmail(); diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index c5b3b28167..d6c04eb8a5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -27,7 +27,22 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.ExceptionHandler; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceInfo; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; @@ -84,6 +99,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; @@ -94,8 +110,8 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.component.ComponentDiscoveryService; +import org.thingsboard.server.service.lwm2m.LwM2MModelsRepository; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; @@ -123,6 +139,9 @@ public abstract class BaseController { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; + protected static final String DEFAULT_DASHBOARD = "defaultDashboardId"; + protected static final String HOME_DASHBOARD = "homeDashboardId"; + private static final ObjectMapper json = new ObjectMapper(); @Autowired @@ -215,6 +234,9 @@ public abstract class BaseController { @Autowired protected TbDeviceProfileCache deviceProfileCache; + @Autowired + protected LwM2MModelsRepository lwM2MModelsRepository; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -645,6 +667,7 @@ public abstract class BaseController { return ruleNode; } + @SuppressWarnings("unchecked") protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } @@ -759,8 +782,9 @@ public abstract class BaseController { entityNode = json.createObjectNode(); if (actionType == ActionType.ATTRIBUTES_UPDATED) { String scope = extractParameter(String.class, 0, additionalInfo); + @SuppressWarnings("unchecked") List attributes = extractParameter(List.class, 1, additionalInfo); - metaData.putValue("scope", scope); + metaData.putValue(DataConstants.SCOPE, scope); if (attributes != null) { for (AttributeKvEntry attr : attributes) { addKvEntry(entityNode, attr); @@ -768,16 +792,19 @@ public abstract class BaseController { } } else if (actionType == ActionType.ATTRIBUTES_DELETED) { String scope = extractParameter(String.class, 0, additionalInfo); + @SuppressWarnings("unchecked") List keys = extractParameter(List.class, 1, additionalInfo); - metaData.putValue("scope", scope); + metaData.putValue(DataConstants.SCOPE, scope); ArrayNode attrsArrayNode = entityNode.putArray("attributes"); if (keys != null) { keys.forEach(attrsArrayNode::add); } } else if (actionType == ActionType.TIMESERIES_UPDATED) { + @SuppressWarnings("unchecked") List timeseries = extractParameter(List.class, 0, additionalInfo); addTimeseries(entityNode, timeseries); } else if (actionType == ActionType.TIMESERIES_DELETED) { + @SuppressWarnings("unchecked") List keys = extractParameter(List.class, 0, additionalInfo); if (keys != null) { ArrayNode timeseriesArrayNode = entityNode.putArray("timeseries"); @@ -853,4 +880,14 @@ public abstract class BaseController { } } } + + protected void processDashboardIdFromAdditionalInfo(ObjectNode additionalInfo, String requiredFields) throws ThingsboardException { + String dashboardId = additionalInfo.has(requiredFields) ? additionalInfo.get(requiredFields).asText() : null; + if(dashboardId != null && !dashboardId.equals("null")) { + if(dashboardService.findDashboardById(getTenantId(), new DashboardId(UUID.fromString(dashboardId))) == null) { + additionalInfo.remove(requiredFields); + } + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 1a2357450c..dd0312169b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -55,7 +55,11 @@ public class CustomerController extends BaseController { checkParameter(CUSTOMER_ID, strCustomerId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); - return checkCustomerId(customerId, Operation.READ); + Customer customer = checkCustomerId(customerId, Operation.READ); + if(!customer.getAdditionalInfo().isNull()) { + processDashboardIdFromAdditionalInfo((ObjectNode) customer.getAdditionalInfo(), HOME_DASHBOARD); + } + return customer; } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index b6c040acdf..2e6efc4246 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.HomeDashboard; +import org.thingsboard.server.common.data.HomeDashboardInfo; import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -38,8 +44,9 @@ import org.thingsboard.server.common.data.id.DashboardId; 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.common.data.page.TimePageLink; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; @@ -53,6 +60,9 @@ public class DashboardController extends BaseController { public static final String DASHBOARD_ID = "dashboardId"; + private static final String HOME_DASHBOARD_ID = "homeDashboardId"; + private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar"; + @Value("${dashboard.max_datapoints_limit}") private long maxDatapointsLimit; @@ -472,4 +482,100 @@ public class DashboardController extends BaseController { throw handleException(e); } } + + @PreAuthorize("isAuthenticated()") + @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET) + @ResponseBody + public HomeDashboard getHomeDashboard() throws ThingsboardException { + try { + SecurityUser securityUser = getCurrentUser(); + if (securityUser.isSystemAdmin()) { + return null; + } + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); + JsonNode additionalInfo = user.getAdditionalInfo(); + HomeDashboard homeDashboard; + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); + if (homeDashboard == null) { + if (securityUser.isCustomerUser()) { + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId()); + additionalInfo = customer.getAdditionalInfo(); + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); + } + if (homeDashboard == null) { + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId()); + additionalInfo = tenant.getAdditionalInfo(); + homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo); + } + } + return homeDashboard; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) + @ResponseBody + public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException { + try { + Tenant tenant = tenantService.findTenantById(getTenantId()); + JsonNode additionalInfo = tenant.getAdditionalInfo(); + DashboardId dashboardId = null; + boolean hideDashboardToolbar = true; + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText(); + dashboardId = new DashboardId(toUUID(strDashboardId)); + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) { + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean(); + } + } + return new HomeDashboardInfo(dashboardId, hideDashboardToolbar); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public void setTenantHomeDashboardInfo(@RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException { + try { + if (homeDashboardInfo.getDashboardId() != null) { + checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ); + } + Tenant tenant = tenantService.findTenantById(getTenantId()); + JsonNode additionalInfo = tenant.getAdditionalInfo(); + if (additionalInfo == null || !(additionalInfo instanceof ObjectNode)) { + additionalInfo = JacksonUtil.OBJECT_MAPPER.createObjectNode(); + } + if (homeDashboardInfo.getDashboardId() != null) { + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_ID, homeDashboardInfo.getDashboardId().getId().toString()); + ((ObjectNode) additionalInfo).put(HOME_DASHBOARD_HIDE_TOOLBAR, homeDashboardInfo.isHideDashboardToolbar()); + } else { + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_ID); + ((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_HIDE_TOOLBAR); + } + tenant.setAdditionalInfo(additionalInfo); + tenantService.saveTenant(tenant); + } catch (Exception e) { + throw handleException(e); + } + } + + private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) { + try { + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText(); + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ); + boolean hideDashboardToolbar = true; + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) { + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean(); + } + return new HomeDashboard(dashboard, hideDashboardToolbar); + } + } catch (Exception e) {} + return null; + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index e965a5a3b2..77bb1846f7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -278,9 +278,7 @@ public class DeviceController extends BaseController { try { Device device = checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(getCurrentUser().getTenantId(), deviceCredentials)); - - tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId()), null); - + tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId(), result), null); logEntityAction(device.getId(), device, device.getCustomerId(), ActionType.CREDENTIALS_UPDATED, null, deviceCredentials); diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceLwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceLwm2mController.java new file mode 100644 index 0000000000..5f2e946f10 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceLwm2mController.java @@ -0,0 +1,129 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig; +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.security.DeviceCredentials; +import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Resource; + +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class DeviceLwm2mController extends BaseController { + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/lwm2m/deviceProfile", params = {"sortOrder", "sortProperty"}, method = RequestMethod.GET) + @ResponseBody + public List getLwm2mListObjects(@RequestParam String sortOrder, + @RequestParam String sortProperty, + @RequestParam(required = false) int[] objectIds, + @RequestParam(required = false) String searchText) + throws ThingsboardException { + try { + return lwM2MModelsRepository.getLwm2mObjects(objectIds, searchText, sortProperty, sortOrder); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/lwm2m/deviceProfile/objects", params = {"pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getLwm2mListObjects(@RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String searchText, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, searchText, sortProperty, sortOrder); + return checkNotNull(lwM2MModelsRepository.findDeviceLwm2mObjects(getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{securityMode}/{bootstrapServerIs}", method = RequestMethod.GET) + @ResponseBody + public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("securityMode") String securityMode, + @PathVariable("bootstrapServerIs") boolean bootstrapServerIs) throws ThingsboardException { + try { + return lwM2MModelsRepository.getBootstrapSecurityInfo(securityMode, bootstrapServerIs); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST) + @ResponseBody + public Device saveDeviceWithCredentials(@RequestBody (required=false) Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { + ObjectMapper mapper = new ObjectMapper(); + Device device = checkNotNull(mapper.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); + DeviceCredentials credentials = checkNotNull(mapper.convertValue( deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); + try { + device.setTenantId(getCurrentUser().getTenantId()); + checkEntity(device.getId(), device, Resource.DEVICE); + Device savedDevice = deviceService.saveDeviceWithCredentials(device, credentials); + checkNotNull(savedDevice); + + tbClusterService.onDeviceChange(savedDevice, null); + tbClusterService.pushMsgToCore(new DeviceNameOrTypeUpdateMsg(savedDevice.getTenantId(), + savedDevice.getId(), savedDevice.getName(), savedDevice.getType()), null); + tbClusterService.onEntityStateChange(savedDevice.getTenantId(), savedDevice.getId(), + device.getId() == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + + logEntityAction(savedDevice.getId(), savedDevice, + savedDevice.getCustomerId(), + device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + if (device.getId() == null) { + deviceStateService.onDeviceAdded(savedDevice); + } else { + deviceStateService.onDeviceUpdated(savedDevice); + } + return savedDevice; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE), device, + null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 2d2342556c..6250975b01 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -63,7 +63,7 @@ import java.util.List; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import static org.apache.commons.lang.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; /** diff --git a/application/src/main/java/org/thingsboard/server/controller/ResourceController.java b/application/src/main/java/org/thingsboard/server/controller/ResourceController.java new file mode 100644 index 0000000000..8939fa28b7 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/ResourceController.java @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.exception.ThingsboardException; +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.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@Slf4j +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class ResourceController extends BaseController { + + private final ResourceService resourceService; + + public ResourceController(ResourceService resourceService) { + this.resourceService = resourceService; + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource", method = RequestMethod.POST) + @ResponseBody + public Resource saveResource(Resource resource) throws ThingsboardException { + try { + resource.setTenantId(getTenantId()); + Resource savedResource = checkNotNull(resourceService.saveResource(resource)); + tbClusterService.onResourceChange(savedResource, null); + return savedResource; + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource", method = RequestMethod.GET) + @ResponseBody + public PageData getResources(@RequestParam(required = false) boolean system, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + try { + PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); + return checkNotNull(resourceService.findResourcesByTenantId(system ? TenantId.SYS_TENANT_ID : getTenantId(), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/resource/{resourceType}/{resourceId}", method = RequestMethod.DELETE) + @ResponseBody + public void deleteResource(@PathVariable("resourceType") ResourceType resourceType, + @PathVariable("resourceId") String resourceId) throws ThingsboardException { + try { + Resource resource = checkNotNull(resourceService.getResource(getTenantId(), resourceType, resourceId)); + resourceService.deleteResource(getTenantId(), resourceType, resourceId); + tbClusterService.onResourceDeleted(resource, null); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index 2bd8a04578..70b5efe40d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -59,7 +60,11 @@ public class TenantController extends BaseController { checkParameter("tenantId", strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); - return checkTenantId(tenantId, Operation.READ); + Tenant tenant = checkTenantId(tenantId, Operation.READ); + if(!tenant.getAdditionalInfo().isNull()) { + processDashboardIdFromAdditionalInfo((ObjectNode) tenant.getAdditionalInfo(), HOME_DASHBOARD); + } + return tenant; } catch (Exception e) { throw handleException(e); } 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 47be552c2f..e1c3328176 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -53,7 +53,6 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; -import org.thingsboard.server.utils.MiscUtils; import javax.servlet.http.HttpServletRequest; @@ -90,12 +89,29 @@ public class UserController extends BaseController { checkParameter(USER_ID, strUserId); try { UserId userId = new UserId(toUUID(strUserId)); - return checkUserId(userId, Operation.READ); + User user = checkUserId(userId, Operation.READ); + if(!user.getAdditionalInfo().isNull()) { + processDashboardIdFromAdditionalInfo((ObjectNode) user.getAdditionalInfo(), DEFAULT_DASHBOARD); + processDashboardIdFromAdditionalInfo((ObjectNode) user.getAdditionalInfo(), HOME_DASHBOARD); + } + UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); + if(userCredentials.isEnabled()) { + addUserCredentialsEnabled((ObjectNode) user.getAdditionalInfo()); + } + return user; } catch (Exception e) { throw handleException(e); } } + private void addUserCredentialsEnabled(ObjectNode additionalInfo) { + if(!additionalInfo.isNull()) { + if(!additionalInfo.has("userCredentialsEnabled")) { + additionalInfo.put("userCredentialsEnabled", true); + } + } + } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET) @ResponseBody @@ -189,13 +205,13 @@ public class UserController extends BaseController { user.getId(), user); UserCredentials userCredentials = userService.findUserCredentialsByUserId(getCurrentUser().getTenantId(), user.getId()); - if (!userCredentials.isEnabled()) { + if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl, userCredentials.getActivateToken()); mailService.sendActivationEmail(activateUrl, email); } else { - throw new ThingsboardException("User is already active!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } } catch (Exception e) { throw handleException(e); @@ -214,13 +230,13 @@ public class UserController extends BaseController { User user = checkUserId(userId, Operation.READ); SecurityUser authUser = getCurrentUser(); UserCredentials userCredentials = userService.findUserCredentialsByUserId(authUser.getTenantId(), user.getId()); - if (!userCredentials.isEnabled()) { + if (!userCredentials.isEnabled() && userCredentials.getActivateToken() != null) { String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request); String activateUrl = String.format(ACTIVATE_URL_PATTERN, baseUrl, userCredentials.getActivateToken()); return activateUrl; } else { - throw new ThingsboardException("User is already active!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + throw new ThingsboardException("User is already activated!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } } catch (Exception e) { throw handleException(e); @@ -329,4 +345,5 @@ public class UserController extends BaseController { throw handleException(e); } } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index ade9797be0..166403f417 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -41,9 +41,13 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint; import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; -import javax.websocket.*; +import javax.websocket.RemoteEndpoint; +import javax.websocket.SendHandler; +import javax.websocket.SendResult; +import javax.websocket.Session; import java.io.IOException; import java.net.URI; +import java.nio.ByteBuffer; import java.security.InvalidParameterException; import java.util.Queue; import java.util.Set; @@ -79,6 +83,9 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr @Value("${server.ws.limits.max_updates_per_session:}") private String perSessionUpdatesConfiguration; + @Value("${server.ws.ping_timeout:30000}") + private long pingTimeout; + private ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); private ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); @@ -120,6 +127,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr return; } internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef, maxMsgQueuePerSession)); + externalSessionMap.put(externalSessionId, internalSessionId); processInWebSocketService(sessionRef, SessionEvent.onEstablished()); log.info("[{}][{}][{}] Session is opened", sessionRef.getSecurityCtx().getTenantId(), externalSessionId, session.getId()); @@ -189,6 +197,8 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr private volatile boolean isSending = false; private final Queue msgQueue; + private volatile long lastActivityTime; + SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef, int maxMsgQueuePerSession) { super(); this.session = session; @@ -196,6 +206,23 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr this.asyncRemote = nativeSession.getAsyncRemote(); this.sessionRef = sessionRef; this.msgQueue = new LinkedBlockingQueue<>(maxMsgQueuePerSession); + this.lastActivityTime = System.currentTimeMillis(); + } + + synchronized void sendPing(long currentTime) { + try { + if (currentTime - lastActivityTime >= pingTimeout) { + this.asyncRemote.sendPing(ByteBuffer.wrap(new byte[]{})); + lastActivityTime = currentTime; + } + } catch (Exception e) { + log.trace("[{}] Failed to send ping msg", session.getId(), e); + try { + close(this.sessionRef, CloseStatus.SESSION_NOT_RELIABLE); + } catch (IOException ioe) { + log.trace("[{}] Session transport error", session.getId(), ioe); + } + } } synchronized void sendMsg(String msg) { @@ -243,6 +270,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr log.trace("[{}] Session transport error", session.getId(), ioe); } } else { + lastActivityTime = System.currentTimeMillis(); String msg = msgQueue.poll(); if (msg != null) { sendMsgInternal(msg); @@ -284,6 +312,22 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr } } + @Override + public void sendPing(TelemetryWebSocketSessionRef sessionRef, long currentTime) throws IOException { + String externalId = sessionRef.getSessionId(); + String internalId = externalSessionMap.get(externalId); + if (internalId != null) { + SessionMetaData sessionMd = internalSessionMap.get(internalId); + if (sessionMd != null) { + sessionMd.sendPing(currentTime); + } else { + log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId); + } + } else { + log.warn("[{}] Failed to find session by external id", externalId); + } + } + @Override public void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus reason) throws IOException { String externalId = sessionRef.getSessionId(); diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index f22dcb0951..63eea2a7fc 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -185,9 +185,21 @@ public class ThingsboardInstallService { case "3.2.0": log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ..."); databaseEntitiesUpgradeService.upgradeDatabase("3.2.0"); + case "3.2.1": + log.info("Upgrading ThingsBoard from version 3.2.1 to 3.2.2 ..."); + if (databaseTsUpgradeService != null) { + databaseTsUpgradeService.upgradeDatabase("3.2.1"); + } + log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; + case "3.2.2": + log.info("Upgrading ThingsBoard from version 3.2.2 to 3.3.0 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.2.2"); + + log.info("Updating system data..."); + break; default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); @@ -220,6 +232,7 @@ public class ThingsboardInstallService { systemDataLoaderService.createAdminSettings(); systemDataLoaderService.loadSystemWidgets(); systemDataLoaderService.createOAuth2Templates(); + systemDataLoaderService.loadSystemLwm2mResources(); // systemDataLoaderService.loadSystemPlugins(); // systemDataLoaderService.loadSystemRules(); diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index d4a5d42320..d0a3984660 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -54,6 +54,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.UsageStatsKVProto; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.telemetry.InternalTelemetryService; @@ -78,7 +79,7 @@ import java.util.stream.Collectors; @Slf4j @Service -public class DefaultTbApiUsageStateService implements TbApiUsageStateService { +public class DefaultTbApiUsageStateService extends TbApplicationEventListener implements TbApiUsageStateService { public static final String HOURLY = "Hourly"; public static final FutureCallback VOID_CALLBACK = new FutureCallback() { @@ -188,7 +189,7 @@ public class DefaultTbApiUsageStateService implements TbApiUsageStateService { } @Override - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { if (partitionChangeEvent.getServiceType().equals(ServiceType.TB_CORE)) { myTenantStates.entrySet().removeIf(entry -> !partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition()); otherTenantStates.entrySet().removeIf(entry -> partitionService.resolve(ServiceType.TB_CORE, entry.getKey(), entry.getKey()).isMyPartition()); diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index bd490efdba..c0a9f5e7c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.stereotype.Service; import org.thingsboard.rule.engine.api.NodeConfiguration; @@ -69,7 +70,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe private ObjectMapper mapper = new ObjectMapper(); private boolean isInstall() { - return environment.acceptsProfiles("install"); + return environment.acceptsProfiles(Profiles.of("install")); } @PostConstruct @@ -185,7 +186,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(nodeAnnotation)); nodeDefinition.setCustomRelations(nodeAnnotation.customRelations()); Class configClazz = nodeAnnotation.configClazz(); - NodeConfiguration config = configClazz.newInstance(); + NodeConfiguration config = configClazz.getDeclaredConstructor().newInstance(); NodeConfiguration defaultConfiguration = config.defaultConfiguration(); nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration)); nodeDefinition.setUiResources(nodeAnnotation.uiResources()); diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index 2793503a81..3fea206c3b 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -50,7 +50,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueCallback; diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java index 76693192cf..0a64a59a08 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java @@ -50,6 +50,7 @@ public class CassandraTsDatabaseUpgradeService extends AbstractCassandraDatabase break; case "2.5.0": case "3.1.1": + case "3.2.1": break; default: throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); 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 659df4e48f..6f257a367e 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 @@ -36,6 +36,9 @@ 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.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; @@ -197,7 +200,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { generalSettings.setKey("general"); ObjectNode node = objectMapper.createObjectNode(); node.put("baseUrl", "http://localhost:8080"); - node.put("prohibitDifferentUrl", true); + node.put("prohibitDifferentUrl", false); generalSettings.setJsonValue(node); adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings); @@ -290,16 +293,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { AlarmCondition temperatureCondition = new AlarmCondition(); temperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - KeyFilter temperatureAlarmFlagAttributeFilter = new KeyFilter(); - temperatureAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperatureAlarmFlag")); + 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); - KeyFilter temperatureTimeseriesFilter = new KeyFilter(); - temperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + 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); @@ -317,8 +320,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { AlarmCondition clearTemperatureCondition = new AlarmCondition(); clearTemperatureCondition.setSpec(new SimpleAlarmConditionSpec()); - KeyFilter clearTemperatureTimeseriesFilter = new KeyFilter(); - clearTemperatureTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + 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); @@ -340,16 +343,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { AlarmCondition humidityCondition = new AlarmCondition(); humidityCondition.setSpec(new SimpleAlarmConditionSpec()); - KeyFilter humidityAlarmFlagAttributeFilter = new KeyFilter(); - humidityAlarmFlagAttributeFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "humidityAlarmFlag")); + 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); - KeyFilter humidityTimeseriesFilter = new KeyFilter(); - humidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity")); + 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); @@ -368,8 +371,8 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { AlarmCondition clearHumidityCondition = new AlarmCondition(); clearHumidityCondition.setSpec(new SimpleAlarmConditionSpec()); - KeyFilter clearHumidityTimeseriesFilter = new KeyFilter(); - clearHumidityTimeseriesFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "humidity")); + 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); @@ -438,9 +441,15 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { this.deleteSystemWidgetBundle("input_widgets"); this.deleteSystemWidgetBundle("date"); this.deleteSystemWidgetBundle("entity_admin_widgets"); + this.deleteSystemWidgetBundle("navigation_widgets"); installScripts.loadSystemWidgets(); } + @Override + public void loadSystemLwm2mResources() throws Exception { + installScripts.loadSystemLwm2mResources(); + } + private User createUser(Authority authority, TenantId tenantId, CustomerId customerId, diff --git a/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java index e8b90ae8fe..b333cafce2 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java @@ -15,11 +15,20 @@ */ package org.thingsboard.server.service.install; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.util.HsqlDao; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; + @Service +@Slf4j @HsqlDao @Profile("install") public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService @@ -27,5 +36,21 @@ public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSe protected HsqlEntityDatabaseSchemaService() { super("schema-entities-hsql.sql", "schema-entities-idx.sql"); } + + private final String schemaTypesSql = "schema-types-hsql.sql"; + + @Override + public void createDatabaseSchema(boolean createIndexes) throws Exception { + + log.info("Installing SQL DataBase types part: " + schemaTypesSql); + + Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaTypesSql); + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8")); + conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema + } + + super.createDatabaseSchema(createIndexes); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 47f8529c11..36b85da719 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -24,15 +24,17 @@ import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.id.CustomerId; 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.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; @@ -42,6 +44,7 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Base64; import java.util.Optional; import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; @@ -66,8 +69,11 @@ public class InstallScripts { public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String DASHBOARDS_DIR = "dashboards"; + public static final String MODELS_DIR = "models"; + public static final String CREDENTIALS_DIR = "credentials"; public static final String JSON_EXT = ".json"; + public static final String XML_EXT = ".xml"; @Value("${install.data_dir:}") private String dataDir; @@ -87,6 +93,9 @@ public class InstallScripts { @Autowired private OAuth2ConfigTemplateService oAuth2TemplateService; + @Autowired + private ResourceService resourceService; + public Path getTenantRuleChainsDir() { return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR); } @@ -186,6 +195,42 @@ public class InstallScripts { } } + public void loadSystemLwm2mResources() throws Exception { + Path modelsDir = Paths.get(getDataDir(), MODELS_DIR); + if (Files.isDirectory(modelsDir)) { + try (DirectoryStream dirStream = Files.newDirectoryStream(modelsDir, path -> path.toString().endsWith(XML_EXT))) { + dirStream.forEach( + path -> { + try { + Resource resource = new Resource(); + resource.setTenantId(TenantId.SYS_TENANT_ID); + resource.setResourceType(ResourceType.LWM2M_MODEL); + resource.setResourceId(path.getFileName().toString()); + resource.setValue(Base64.getEncoder().encodeToString(Files.readAllBytes(path))); + resourceService.saveResource(resource); + } catch (Exception e) { + log.error("Unable to load lwm2m model [{}]", path.toString()); + throw new RuntimeException("Unable to load lwm2m model", e); + } + } + ); + } + } + + Path jksPath = Paths.get(getDataDir(), CREDENTIALS_DIR, "serverKeyStore.jks"); + try { + Resource resource = new Resource(); + resource.setTenantId(TenantId.SYS_TENANT_ID); + resource.setResourceType(ResourceType.JKS); + resource.setResourceId(jksPath.getFileName().toString()); + resource.setValue(Base64.getEncoder().encodeToString(Files.readAllBytes(jksPath))); + resourceService.saveResource(resource); + } catch (Exception e) { + log.error("Unable to load lwm2m serverKeyStore [{}]", jksPath.toString()); + throw new RuntimeException("Unable to load l2m2m serverKeyStore", e); + } + } + public void loadDashboards(TenantId tenantId, CustomerId customerId) throws Exception { Path dashboardsDir = Paths.get(getDataDir(), JSON_DIR, DEMO_DIR, DASHBOARDS_DIR); try (DirectoryStream dirStream = Files.newDirectoryStream(dashboardsDir, path -> path.toString().endsWith(JSON_EXT))) { @@ -208,7 +253,6 @@ public class InstallScripts { } } - public void loadDemoRuleChains(TenantId tenantId) throws Exception { try { createDefaultRuleChains(tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java index 1aed8c01c4..fddec0367d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java @@ -196,11 +196,17 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe } break; case "3.1.1": + case "3.2.1": try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { log.info("Load TTL functions ..."); loadSql(conn, LOAD_TTL_FUNCTIONS_SQL); log.info("Load Drop Partitions functions ..."); loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL); + + executeQuery(conn, "DROP PROCEDURE IF EXISTS cleanup_timeseries_by_ttl(character varying, bigint, bigint);"); + executeQuery(conn, "DROP FUNCTION IF EXISTS delete_asset_records_from_ts_kv(character varying, character varying, bigint);"); + executeQuery(conn, "DROP FUNCTION IF EXISTS delete_device_records_from_ts_kv(character varying, character varying, bigint);"); + executeQuery(conn, "DROP FUNCTION IF EXISTS delete_customer_records_from_ts_kv(character varying, character varying, bigint);"); } break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index f823c40e69..1e652880a0 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java @@ -30,7 +30,7 @@ import java.sql.SQLException; @Slf4j public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchemaService { - private static final String SQL_DIR = "sql"; + protected static final String SQL_DIR = "sql"; @Value("${spring.datasource.url}") protected String dbUrl; @@ -42,7 +42,7 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema protected String dbPassword; @Autowired - private InstallScripts installScripts; + protected InstallScripts installScripts; private final String schemaSql; private final String schemaIdxSql; diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index c3495d960b..a56cc7ca25 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -434,6 +434,25 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Schema updated."); } break; + case "3.2.2": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + try { + conn.createStatement().execute("CREATE TABLE IF NOT EXISTS resource (" + + " tenant_id uuid NOT NULL," + + " resource_type varchar(32) NOT NULL," + + " resource_id varchar(255) NOT NULL," + + " resource_value varchar," + + " CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id)" + + " );"); + + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;"); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + log.info("Schema updated."); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index 73e2b6ea57..a6f33f476f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -33,4 +33,6 @@ public interface SystemDataLoaderService { void deleteSystemWidgetBundle(String bundleAlias) throws Exception; + void loadSystemLwm2mResources() throws Exception; + } diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index 0920c2c07d..112ecc3018 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java @@ -178,6 +178,7 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr } break; case "3.1.1": + case "3.2.1": break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index 606bb34432..cb987a5e65 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java @@ -146,17 +146,17 @@ public class CassandraDbHelper { if (row.isNull(index)) { return null; } else if (type.getProtocolCode() == ProtocolConstants.DataType.DOUBLE) { - str = new Double(row.getDouble(index)).toString(); + str = Double.valueOf(row.getDouble(index)).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.INT) { - str = new Integer(row.getInt(index)).toString(); + str = Integer.valueOf(row.getInt(index)).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.BIGINT) { - str = new Long(row.getLong(index)).toString(); + str = Long.valueOf(row.getLong(index)).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.UUID) { str = row.getUuid(index).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMEUUID) { str = row.getUuid(index).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.FLOAT) { - str = new Float(row.getFloat(index)).toString(); + str = Float.valueOf(row.getFloat(index)).toString(); } else if (type.getProtocolCode() == ProtocolConstants.DataType.TIMESTAMP) { str = ""+row.getInstant(index).toEpochMilli(); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java index bc53134973..f841621bbc 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraToSqlColumn.java @@ -153,7 +153,8 @@ public class CassandraToSqlColumn { sqlInsertStatement.setBoolean(this.sqlIndex, Boolean.parseBoolean(value)); break; case ENUM_TO_INT: - Enum enumVal = Enum.valueOf(this.enumClass, value); + @SuppressWarnings("unchecked") + Enum enumVal = Enum.valueOf(this.enumClass, value); int intValue = enumVal.ordinal(); sqlInsertStatement.setInt(this.sqlIndex, intValue); break; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 3eebf8ff93..f232c0a41e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.service.install.update; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -25,16 +23,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration; import org.thingsboard.server.common.data.EntityView; -import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -47,18 +41,16 @@ import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.service.install.InstallScripts; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import static org.apache.commons.lang.StringUtils.isBlank; -import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper; +import static org.apache.commons.lang3.StringUtils.isBlank; @Service @Profile("install") diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MModelsRepository.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MModelsRepository.java new file mode 100644 index 0000000000..ddadc6c95c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MModelsRepository.java @@ -0,0 +1,314 @@ +/** + * Copyright © 2016-2021 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.lwm2m; + + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.util.Hex; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.data.domain.PageImpl; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; +import org.thingsboard.server.common.data.lwm2m.LwM2mObject; +import org.thingsboard.server.common.data.lwm2m.LwM2mResource; +import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigBootstrap; +import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigServer; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyStoreException; +import java.security.PublicKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.KeySpec; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Slf4j +@Service +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") +public class LwM2MModelsRepository { + + private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + + @Autowired + LwM2MTransportConfigServer contextServer; + + + @Autowired + LwM2MTransportConfigBootstrap contextBootStrap; + + /** + * @param objectIds + * @param textSearch + * @return list of LwM2mObject + * Filter by Predicate (uses objectIds, if objectIds is null then it uses textSearch, + * if textSearch is null then it uses AllList from List) + */ + public List getLwm2mObjects(int[] objectIds, String textSearch, String sortProperty, String sortOrder) { + if (objectIds == null && textSearch != null && !textSearch.isEmpty()) { + objectIds = getObjectIdFromTextSearch(textSearch); + } + int[] finalObjectIds = objectIds; + return getLwm2mObjects((objectIds != null && objectIds.length > 0 && textSearch != null && !textSearch.isEmpty()) ? + (ObjectModel element) -> IntStream.of(finalObjectIds).anyMatch(x -> x == element.id) || element.name.toLowerCase().contains(textSearch.toLowerCase()) : + (objectIds != null && objectIds.length > 0) ? + (ObjectModel element) -> IntStream.of(finalObjectIds).anyMatch(x -> x == element.id) : + (textSearch != null && !textSearch.isEmpty()) ? + (ObjectModel element) -> element.name.contains(textSearch) : + null, + sortProperty, sortOrder); + } + + /** + * @param predicate + * @return list of LwM2mObject + */ + private List getLwm2mObjects(Predicate predicate, String sortProperty, String sortOrder) { + List lwM2mObjects = new ArrayList<>(); + List listObjects = (predicate == null) ? this.contextServer.getModelsValue() : + contextServer.getModelsValue().stream() + .filter(predicate) + .collect(Collectors.toList()); + + listObjects.forEach(obj -> { + LwM2mObject lwM2mObject = new LwM2mObject(); + lwM2mObject.setId(obj.id); + lwM2mObject.setName(obj.name); + lwM2mObject.setMultiple(obj.multiple); + lwM2mObject.setMandatory(obj.mandatory); + LwM2mInstance instance = new LwM2mInstance(); + instance.setId(0); + List resources = new ArrayList<>(); + obj.resources.forEach((k, v) -> { + if (!v.operations.isExecutable()) { + LwM2mResource resource = new LwM2mResource(k, v.name, false, false, false); + resources.add(resource); + } + }); + instance.setResources(resources.stream().toArray(LwM2mResource[]::new)); + lwM2mObject.setInstances(new LwM2mInstance[]{instance}); + lwM2mObjects.add(lwM2mObject); + }); + return lwM2mObjects.size() > 1 ? this.sortList (lwM2mObjects, sortProperty, sortOrder) : lwM2mObjects; + } + + private List sortList (List lwM2mObjects, String sortProperty, String sortOrder) { + switch (sortProperty) { + case "name": + switch (sortOrder) { + case "ASC": + lwM2mObjects.sort((o1, o2) -> o1.getName().compareTo(o2.getName())); + break; + case "DESC": + lwM2mObjects.stream().sorted(Comparator.comparing(LwM2mObject::getName).reversed()); + break; + } + case "id": + switch (sortOrder) { + case "ASC": + lwM2mObjects.sort((o1, o2) -> Long.compare(o1.getId(), o2.getId())); + break; + case "DESC": + lwM2mObjects.sort((o1, o2) -> Long.compare(o2.getId(), o1.getId())); + } + } + return lwM2mObjects; + } + + /** + * @param tenantId + * @param pageLink + * @return List of LwM2mObject in PageData format + */ + public PageData findDeviceLwm2mObjects(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findDeviceProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validatePageLink(pageLink); + return this.findLwm2mListObjects(pageLink); + } + + /** + * @param pageLink + * @return List of LwM2mObject in PageData format, filter == TextSearch + * PageNumber = 1, PageSize = List.size() + */ + public PageData findLwm2mListObjects(PageLink pageLink) { + PageImpl page = new PageImpl<>(getLwm2mObjects(getObjectIdFromTextSearch(pageLink.getTextSearch()), + pageLink.getTextSearch(), + pageLink.getSortOrder().getProperty(), + pageLink.getSortOrder().getDirection().name())); + PageData pageData = new PageData<>(page.getContent(), page.getTotalPages(), page.getTotalElements(), page.hasNext()); + return pageData; + } + + /** + * Filter for id Object + * @param textSearch - + * @return - return Object id only first chartAt in textSearch + */ + private int[] getObjectIdFromTextSearch(String textSearch) { + String filtered = null; + if (textSearch !=null && !textSearch.isEmpty()) { + AtomicInteger a = new AtomicInteger(); + filtered = textSearch.chars () + .mapToObj(chr -> (char) chr) + .filter(i -> Character.isDigit(i) && textSearch.charAt(a.getAndIncrement()) == i) + .collect(Collector.of(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString)); + } + return (filtered != null && !filtered.isEmpty()) ? new int[]{Integer.parseInt(filtered)} : new int[0]; + } + + /** + * @param securityMode + * @param bootstrapServerIs + * @return ServerSecurityConfig more value is default: Important - port, host, publicKey + */ + public ServerSecurityConfig getBootstrapSecurityInfo(String securityMode, boolean bootstrapServerIs) { + LwM2MSecurityMode lwM2MSecurityMode = LwM2MSecurityMode.fromSecurityMode(securityMode.toLowerCase()); + return getBootstrapServer(bootstrapServerIs, lwM2MSecurityMode); + } + + /** + * @param bootstrapServerIs + * @param mode + * @return ServerSecurityConfig more value is default: Important - port, host, publicKey + */ + private ServerSecurityConfig getBootstrapServer(boolean bootstrapServerIs, LwM2MSecurityMode mode) { + ServerSecurityConfig bsServ = new ServerSecurityConfig(); + bsServ.setBootstrapServerIs(bootstrapServerIs); + if (bootstrapServerIs) { + bsServ.setServerId(contextBootStrap.getBootstrapServerId()); + switch (mode) { + case NO_SEC: + bsServ.setHost(contextBootStrap.getBootstrapHost()); + bsServ.setPort(contextBootStrap.getBootstrapPortNoSec()); + bsServ.setServerPublicKey(""); + break; + case PSK: + bsServ.setHost(contextBootStrap.getBootstrapHostSecurity()); + bsServ.setPort(contextBootStrap.getBootstrapPortSecurity()); + bsServ.setServerPublicKey(""); + break; + case RPK: + case X509: + bsServ.setHost(contextBootStrap.getBootstrapHostSecurity()); + bsServ.setPort(contextBootStrap.getBootstrapPortSecurity()); + bsServ.setServerPublicKey(getPublicKey (contextBootStrap.getBootstrapAlias(), this.contextBootStrap.getBootstrapPublicX(), this.contextBootStrap.getBootstrapPublicY())); + break; + default: + break; + } + } else { + bsServ.setServerId(contextServer.getServerId()); + switch (mode) { + case NO_SEC: + bsServ.setHost(contextServer.getServerHost()); + bsServ.setPort(contextServer.getServerPortNoSec()); + bsServ.setServerPublicKey(""); + break; + case PSK: + bsServ.setHost(contextServer.getServerHostSecurity()); + bsServ.setPort(contextServer.getServerPortSecurity()); + bsServ.setServerPublicKey(""); + break; + case RPK: + case X509: + bsServ.setHost(contextServer.getServerHostSecurity()); + bsServ.setPort(contextServer.getServerPortSecurity()); + bsServ.setServerPublicKey(getPublicKey (contextServer.getServerAlias(), this.contextServer.getServerPublicX(), this.contextServer.getServerPublicY())); + break; + default: + break; + } + } + return bsServ; + } + + private String getPublicKey (String alias, String publicServerX, String publicServerY) { + String publicKey = getServerPublicKeyX509(alias); + return publicKey != null ? publicKey : getRPKPublicKey(publicServerX, publicServerY); + } + + /** + * @param alias + * @return PublicKey format HexString or null + */ + private String getServerPublicKeyX509(String alias) { + try { + X509Certificate serverCertificate = (X509Certificate) contextServer.getKeyStoreValue().getCertificate(alias); + return Hex.encodeHexString(serverCertificate.getEncoded()); + } catch (CertificateEncodingException | KeyStoreException e) { + e.printStackTrace(); + } + return null; + } + + /** + * @param publicServerX + * @param publicServerY + * @return PublicKey format HexString or null + */ + private String getRPKPublicKey(String publicServerX, String publicServerY) { + try { + /** Get Elliptic Curve Parameter spec for secp256r1 */ + AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); + algoParameters.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); + if (publicServerX != null && !publicServerX.isEmpty() && publicServerY != null && !publicServerY.isEmpty()) { + /** Get point values */ + byte[] publicX = Hex.decodeHex(publicServerX.toCharArray()); + byte[] publicY = Hex.decodeHex(publicServerY.toCharArray()); + /** Create key specs */ + KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), + parameterSpec); + /** Get keys */ + PublicKey publicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); + if (publicKey != null && publicKey.getEncoded().length > 0) { + return Hex.encodeHexString(publicKey.getEncoded()); + } + } + } catch (GeneralSecurityException | IllegalArgumentException e) { + log.error("[{}] Failed generate Server RPK for profile", e.getMessage()); + throw new RuntimeException(e); + } + return null; + } +} + diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 39a1c46325..22658563e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -47,7 +47,7 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.security.AccessValidator; @@ -206,7 +206,7 @@ public class DefaultEntityQueryService implements EntityQueryService { addItemsToArrayNode(json.putArray("entityTypes"), types); addItemsToArrayNode(json.putArray("timeseries"), timeseriesKeys); addItemsToArrayNode(json.putArray("attribute"), attributesKeys); - response.setResult(new ResponseEntity(json, HttpStatus.OK)); + response.setResult(new ResponseEntity<>(json, HttpStatus.OK)); } private void replyWithEmptyResponse(DeferredResult response) { 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 51fd388af9..3405ab28d8 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 @@ -34,6 +34,7 @@ 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.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.data.transport.resource.Resource; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; @@ -145,7 +146,7 @@ public class DefaultTbClusterService implements TbClusterService { tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId()))); } } - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, entityId); + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId); log.trace("PUSHING msg: {} to:{}", tbMsg, tpi); ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) @@ -247,6 +248,34 @@ public class DefaultTbClusterService implements TbClusterService { onEntityDelete(entity.getTenantId(), entity.getId(), entity.getName(), callback); } + @Override + public void onResourceChange(Resource resource, TbQueueCallback callback) { + TenantId tenantId = resource.getTenantId(); + log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceId()); + TransportProtos.ResourceUpdateMsg resourceUpdateMsg = TransportProtos.ResourceUpdateMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setResourceType(resource.getResourceType().name()) + .setResourceId(resource.getResourceId()) + .build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); + broadcast(transportMsg, callback); + } + + @Override + public void onResourceDeleted(Resource resource, TbQueueCallback callback) { + TenantId tenantId = resource.getTenantId(); + log.trace("[{}][{}][{}] Processing delete resource", tenantId, resource.getResourceType(), resource.getResourceId()); + TransportProtos.ResourceDeleteMsg resourceUpdateMsg = TransportProtos.ResourceDeleteMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) + .setResourceType(resource.getResourceType().name()) + .setResourceId(resource.getResourceId()) + .build(); + ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceUpdateMsg).build(); + broadcast(transportMsg, callback); + } + public void onEntityChange(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); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index df0e7f86b5..02f69ce10d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; @@ -151,12 +151,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService tpi.newByTopic(usageStatsConsumer.getTopic())) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index 98e46d5a50..390798a3e2 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -140,11 +140,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< } @Override - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { - if (partitionChangeEvent.getServiceType().equals(getServiceType())) { - ServiceQueue serviceQueue = partitionChangeEvent.getServiceQueueKey().getServiceQueue(); - log.info("[{}] Subscribing to partitions: {}", serviceQueue.getQueue(), partitionChangeEvent.getPartitions()); - consumers.get(serviceQueue.getQueue()).subscribe(partitionChangeEvent.getPartitions()); + protected void onTbApplicationEvent(PartitionChangeEvent event) { + if (event.getServiceType().equals(getServiceType())) { + ServiceQueue serviceQueue = event.getServiceQueueKey().getServiceQueue(); + log.info("[{}] Subscribing to partitions: {}", serviceQueue.getQueue(), event.getPartitions()); + consumers.get(serviceQueue.getQueue()).subscribe(event.getPartitions()); } } @@ -181,7 +181,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< new TbMsgPackCallback(id, tenantId, ctx, stats.getTimer(tenantId, SUCCESSFUL_STATUS), stats.getTimer(tenantId, FAILED_STATUS)) : new TbMsgPackCallback(id, tenantId, ctx); try { - if (toRuleEngineMsg.getTbMsg() != null && !toRuleEngineMsg.getTbMsg().isEmpty()) { + if (!toRuleEngineMsg.getTbMsg().isEmpty()) { forwardToRuleEngineActor(configuration.getName(), tenantId, toRuleEngineMsg, callback); } else { callback.onSuccess(); @@ -209,6 +209,9 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< if (statsEnabled) { stats.log(result, decision.isCommit()); } + + ctx.cleanup(); + if (decision.isCommit()) { submitStrategy.stop(); break; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java index 92401ea9c0..c1cafa3379 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.TenantProfile; 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.transport.resource.Resource; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos; @@ -71,4 +72,8 @@ public interface TbClusterService { void onDeviceChange(Device device, TbQueueCallback callback); void onDeviceDeleted(Device device, TbQueueCallback callback); + + void onResourceChange(Resource resource, TbQueueCallback callback); + + void onResourceDeleted(Resource resource, TbQueueCallback callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java index d342b4b565..bca5a90fa8 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbCoreConsumerStats.java @@ -55,31 +55,22 @@ public class TbCoreConsumerStats { public TbCoreConsumerStats(StatsFactory statsFactory) { String statsKey = StatsType.CORE.getName(); - this.totalCounter = statsFactory.createStatsCounter(statsKey, TOTAL_MSGS); - this.sessionEventCounter = statsFactory.createStatsCounter(statsKey, SESSION_EVENTS); - this.getAttributesCounter = statsFactory.createStatsCounter(statsKey, GET_ATTRIBUTE); - this.subscribeToAttributesCounter = statsFactory.createStatsCounter(statsKey, ATTRIBUTE_SUBSCRIBES); - this.subscribeToRPCCounter = statsFactory.createStatsCounter(statsKey, RPC_SUBSCRIBES); - this.toDeviceRPCCallResponseCounter = statsFactory.createStatsCounter(statsKey, TO_DEVICE_RPC_CALL_RESPONSES); - this.subscriptionInfoCounter = statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_INFO); - this.claimDeviceCounter = statsFactory.createStatsCounter(statsKey, DEVICE_CLAIMS); - this.deviceStateCounter = statsFactory.createStatsCounter(statsKey, DEVICE_STATES); - this.subscriptionMsgCounter = statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS); - this.toCoreNotificationsCounter = statsFactory.createStatsCounter(statsKey, TO_CORE_NOTIFICATIONS); - - - counters.add(totalCounter); - counters.add(sessionEventCounter); - counters.add(getAttributesCounter); - counters.add(subscribeToAttributesCounter); - counters.add(subscribeToRPCCounter); - counters.add(toDeviceRPCCallResponseCounter); - counters.add(subscriptionInfoCounter); - counters.add(claimDeviceCounter); + this.totalCounter = register(statsFactory.createStatsCounter(statsKey, TOTAL_MSGS)); + this.sessionEventCounter = register(statsFactory.createStatsCounter(statsKey, SESSION_EVENTS)); + this.getAttributesCounter = register(statsFactory.createStatsCounter(statsKey, GET_ATTRIBUTE)); + this.subscribeToAttributesCounter = register(statsFactory.createStatsCounter(statsKey, ATTRIBUTE_SUBSCRIBES)); + this.subscribeToRPCCounter = register(statsFactory.createStatsCounter(statsKey, RPC_SUBSCRIBES)); + this.toDeviceRPCCallResponseCounter = register(statsFactory.createStatsCounter(statsKey, TO_DEVICE_RPC_CALL_RESPONSES)); + this.subscriptionInfoCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_INFO)); + this.claimDeviceCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_CLAIMS)); + this.deviceStateCounter = register(statsFactory.createStatsCounter(statsKey, DEVICE_STATES)); + this.subscriptionMsgCounter = register(statsFactory.createStatsCounter(statsKey, SUBSCRIPTION_MSGS)); + this.toCoreNotificationsCounter = register(statsFactory.createStatsCounter(statsKey, TO_CORE_NOTIFICATIONS)); + } - counters.add(deviceStateCounter); - counters.add(subscriptionMsgCounter); - counters.add(toCoreNotificationsCounter); + private StatsCounter register(StatsCounter counter){ + counters.add(counter); + return counter; } public void log(TransportProtos.TransportToDeviceActorMsg msg) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java index 4ccdce8ff8..d7a064c4ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java @@ -147,4 +147,10 @@ public class TbMsgPackProcessingContext { .forEach(info -> log.info("[{}][{}] execution count: {}. {}", queueName, info.getRuleNodeId(), info.getExecutionCount(), info.getLabel())); } } + + public void cleanup() { + pendingMap.clear(); + successMap.clear(); + failedMap.clear(); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java index 31d5cf47c3..02378eb557 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java @@ -36,6 +36,7 @@ import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -56,7 +57,7 @@ import java.util.function.Function; import java.util.stream.Collectors; @Slf4j -public abstract class AbstractConsumerService implements ApplicationListener { +public abstract class AbstractConsumerService extends TbApplicationEventListener { protected volatile ExecutorService consumersExecutor; protected volatile ExecutorService notificationsConsumerExecutor; diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java b/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java index b75cf4d7ca..64a08b2ef4 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/ToDeviceRpcRequestActorMsg.java @@ -31,6 +31,8 @@ import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; @RequiredArgsConstructor public class ToDeviceRpcRequestActorMsg implements ToDeviceActorNotificationMsg { + private static final long serialVersionUID = -8592877558138716589L; + @Getter private final String serviceId; @Getter diff --git a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java index 4aae3263d5..1d7a5d9533 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java +++ b/application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandboxes; -import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -33,6 +32,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.script.Invocable; import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -97,8 +97,8 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer sandbox.allowLoadFunctions(true); sandbox.setMaxPreparedStatements(30); } else { - NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); - engine = factory.getScriptEngine(new String[]{"--no-java"}); + ScriptEngineManager factory = new ScriptEngineManager(); + engine = factory.getEngineByName("nashorn"); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java index d9f4b976eb..331bbc6ef6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/SkipPathRequestMatcher.java @@ -29,7 +29,7 @@ public class SkipPathRequestMatcher implements RequestMatcher { private RequestMatcher processingMatcher; public SkipPathRequestMatcher(List pathsToSkip, String processingPath) { - Assert.notNull(pathsToSkip); + Assert.notNull(pathsToSkip, "List of paths to skip is required."); List m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path)).collect(Collectors.toList()); matchers = new OrRequestMatcher(m); processingMatcher = new AntPathRequestMatcher(processingPath); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java new file mode 100644 index 0000000000..dc66bcca8e --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CookieUtils.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2021 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.security.auth.oauth2; + +import org.springframework.util.SerializationUtils; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Base64; +import java.util.Optional; + +public class CookieUtils { + + public static Optional getCookie(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return Optional.of(cookie); + } + } + } + + return Optional.empty(); + } + + public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + + public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null && cookies.length > 0) { + for (Cookie cookie: cookies) { + if (cookie.getName().equals(name)) { + cookie.setValue(""); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } + } + } + + public static String serialize(Object object) { + return Base64.getUrlEncoder() + .encodeToString(SerializationUtils.serialize(object)); + } + + public static T deserialize(Cookie cookie, Class cls) { + return cls.cast(SerializationUtils.deserialize( + Base64.getUrlDecoder().decode(cookie.getValue()))); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java new file mode 100644 index 0000000000..27534fe33c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2021 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.security.auth.oauth2; + +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { + public static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request"; + private static final int cookieExpireSeconds = 180; + + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + return CookieUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME) + .map(cookie -> CookieUtils.deserialize(cookie, OAuth2AuthorizationRequest.class)) + .orElse(null); + } + + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) { + if (authorizationRequest == null) { + CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + return; + } + CookieUtils.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtils.serialize(authorizationRequest), cookieExpireSeconds); + } + + @SuppressWarnings("deprecation") + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { + return this.loadAuthorizationRequest(request); + } + + public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) { + CookieUtils.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java index 98d7fd9a20..b6345f1618 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java @@ -37,10 +37,13 @@ import java.nio.charset.StandardCharsets; @ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; private final SystemSecurityService systemSecurityService; @Autowired - public Oauth2AuthenticationFailureHandler(final SystemSecurityService systemSecurityService) { + public Oauth2AuthenticationFailureHandler(final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository, + final SystemSecurityService systemSecurityService) { + this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository; this.systemSecurityService = systemSecurityService; } @@ -49,6 +52,7 @@ public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationF HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String baseUrl = this.systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request); + httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/login?loginError=" + URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8.toString())); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 29ef86bcc0..3cb75501d9 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -49,6 +49,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS private final OAuth2ClientMapperProvider oauth2ClientMapperProvider; private final OAuth2Service oAuth2Service; private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService; + private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; private final SystemSecurityService systemSecurityService; @Autowired @@ -56,12 +57,15 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS final RefreshTokenRepository refreshTokenRepository, final OAuth2ClientMapperProvider oauth2ClientMapperProvider, final OAuth2Service oAuth2Service, - final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, final SystemSecurityService systemSecurityService) { + final OAuth2AuthorizedClientService oAuth2AuthorizedClientService, + final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository, + final SystemSecurityService systemSecurityService) { this.tokenFactory = tokenFactory; this.refreshTokenRepository = refreshTokenRepository; this.oauth2ClientMapperProvider = oauth2ClientMapperProvider; this.oAuth2Service = oAuth2Service; this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService; + this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository; this.systemSecurityService = systemSecurityService; } @@ -84,10 +88,17 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); + clearAuthenticationAttributes(request, response); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); } catch (Exception e) { + clearAuthenticationAttributes(request, response); getRedirectStrategy().sendRedirect(request, response, baseUrl + "/login?loginError=" + URLEncoder.encode(e.getMessage(), StandardCharsets.UTF_8.toString())); } } + + protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) { + super.clearAuthenticationAttributes(request); + httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response); + } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java index 7bc8d7c043..5789d09c5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/device/DefaultDeviceAuthService.java @@ -53,6 +53,8 @@ public class DefaultDeviceAuthService implements DeviceAuthService { return DeviceAuthResult.of(credentials.getDeviceId()); case X509_CERTIFICATE: return DeviceAuthResult.of(credentials.getDeviceId()); + case LWM2M_CREDENTIALS: + return DeviceAuthResult.of(credentials.getDeviceId()); default: return DeviceAuthResult.of("Credentials Type is not supported yet!"); } @@ -65,4 +67,4 @@ public class DefaultDeviceAuthService implements DeviceAuthService { } } -} \ No newline at end of file +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index b4e26c1447..cfbf051cad 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -100,6 +100,7 @@ public class JwtTokenFactory { Jws jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey()); Claims claims = jwsClaims.getBody(); String subject = claims.getSubject(); + @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); if (scopes == null || scopes.isEmpty()) { throw new IllegalArgumentException("JWT Token doesn't have any scopes"); @@ -155,6 +156,7 @@ public class JwtTokenFactory { Jws jwsClaims = rawAccessToken.parseClaims(settings.getTokenSigningKey()); Claims claims = jwsClaims.getBody(); String subject = claims.getSubject(); + @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); if (scopes == null || scopes.isEmpty()) { throw new IllegalArgumentException("Refresh Token doesn't have any scopes"); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java index a92b50cda8..4b2c22f501 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java @@ -47,6 +47,7 @@ public class CustomerUserPermissions extends AbstractPermissions { Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY, Operation.RPC_CALL, Operation.CLAIM_DEVICES) { @Override + @SuppressWarnings("unchecked") public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { if (!super.hasPermission(user, operation, entityId, entity)) { @@ -69,6 +70,7 @@ public class CustomerUserPermissions extends AbstractPermissions { new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) { @Override + @SuppressWarnings("unchecked") public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { if (!super.hasPermission(user, operation, entityId, entity)) { return false; @@ -119,6 +121,7 @@ public class CustomerUserPermissions extends AbstractPermissions { private static final PermissionChecker widgetsPermissionChecker = new PermissionChecker.GenericPermissionChecker(Operation.READ) { @Override + @SuppressWarnings("unchecked") public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { if (!super.hasPermission(user, operation, entityId, entity)) { return false; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java index 624e1743ca..0e56963464 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/DefaultAccessControlService.java @@ -56,6 +56,7 @@ public class DefaultAccessControlService implements AccessControlService { } @Override + @SuppressWarnings("unchecked") public void checkPermission(SecurityUser user, Resource resource, Operation operation, I entityId, T entity) throws ThingsboardException { PermissionChecker permissionChecker = getPermissionChecker(user.getAuthority(), resource); diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index b8d2c539cd..f56e1e0554 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -59,6 +59,7 @@ public class TenantAdminPermissions extends AbstractPermissions { new PermissionChecker.GenericPermissionChecker(Operation.READ, Operation.READ_ATTRIBUTES, Operation.READ_TELEMETRY) { @Override + @SuppressWarnings("unchecked") public boolean hasPermission(SecurityUser user, Operation operation, EntityId entityId, HasTenantId entity) { if (!super.hasPermission(user, operation, entityId, entity)) { return false; diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 9ebff31ad4..5db9509a77 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.service.security.system; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -49,6 +49,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserServiceImpl; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; import org.thingsboard.server.utils.MiscUtils; @@ -65,8 +66,6 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING @Slf4j public class DefaultSystemSecurityService implements SystemSecurityService { - private static final ObjectMapper objectMapper = new ObjectMapper(); - @Autowired private AdminSettingsService adminSettingsService; @@ -89,7 +88,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings"); if (adminSettings != null) { try { - securitySettings = objectMapper.treeToValue(adminSettings.getJsonValue(), SecuritySettings.class); + securitySettings = JacksonUtil.convertValue(adminSettings.getJsonValue(), SecuritySettings.class); } catch (Exception e) { throw new RuntimeException("Failed to load security settings!", e); } @@ -109,10 +108,10 @@ public class DefaultSystemSecurityService implements SystemSecurityService { adminSettings = new AdminSettings(); adminSettings.setKey("securitySettings"); } - adminSettings.setJsonValue(objectMapper.valueToTree(securitySettings)); + adminSettings.setJsonValue(JacksonUtil.valueToTree(securitySettings)); AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings); try { - return objectMapper.treeToValue(savedAdminSettings.getJsonValue(), SecuritySettings.class); + return JacksonUtil.convertValue(savedAdminSettings.getJsonValue(), SecuritySettings.class); } catch (Exception e) { throw new RuntimeException("Failed to load security settings!", e); } @@ -189,7 +188,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService { JsonNode additionalInfo = user.getAdditionalInfo(); if (additionalInfo instanceof ObjectNode && additionalInfo.has(UserServiceImpl.USER_PASSWORD_HISTORY)) { JsonNode userPasswordHistoryJson = additionalInfo.get(UserServiceImpl.USER_PASSWORD_HISTORY); - Map userPasswordHistoryMap = objectMapper.convertValue(userPasswordHistoryJson, Map.class); + Map userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); for (Map.Entry entry : userPasswordHistoryMap.entrySet()) { if (encoder.matches(password, entry.getValue()) && Long.parseLong(entry.getKey()) > passwordReuseFrequencyTs) { throw new DataValidationException("Password was already used for the last " + passwordPolicy.getPasswordReuseFrequencyDays() + " days"); diff --git a/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java index 1a7619aec8..84aa972e8a 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/AbstractSmsSender.java @@ -24,7 +24,7 @@ import java.util.regex.Pattern; @Slf4j public abstract class AbstractSmsSender implements SmsSender { - private static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$"); + protected static final Pattern E_164_PHONE_NUMBER_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$"); private static final int MAX_SMS_MESSAGE_LENGTH = 1600; private static final int MAX_SMS_SEGMENT_LENGTH = 70; diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java index af72b909ca..f7a012b23e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.settings.AdminSettingsService; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.queue.usagestats.TbApiUsageClient; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; diff --git a/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java index 27c441410c..1988013fb0 100644 --- a/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java +++ b/application/src/main/java/org/thingsboard/server/service/sms/twilio/TwilioSmsSender.java @@ -19,21 +19,34 @@ import com.twilio.http.TwilioRestClient; import com.twilio.rest.api.v2010.account.Message; import com.twilio.type.PhoneNumber; import org.apache.commons.lang3.StringUtils; +import org.thingsboard.rule.engine.api.sms.exception.SmsParseException; import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration; import org.thingsboard.rule.engine.api.sms.exception.SmsException; import org.thingsboard.rule.engine.api.sms.exception.SmsSendException; import org.thingsboard.server.service.sms.AbstractSmsSender; +import java.util.regex.Pattern; + public class TwilioSmsSender extends AbstractSmsSender { + private static final Pattern PHONE_NUMBERS_SID_MESSAGE_SERVICE_SID = Pattern.compile("^(PN|MG).*$"); + private TwilioRestClient twilioRestClient; private String numberFrom; + private String validatePhoneTwilioNumber(String phoneNumber) throws SmsParseException { + phoneNumber = phoneNumber.trim(); + if (!E_164_PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches() && !PHONE_NUMBERS_SID_MESSAGE_SERVICE_SID.matcher(phoneNumber).matches()) { + throw new SmsParseException("Invalid phone number format. Phone number must be in E.164 format/Phone Number's SID/Messaging Service SID."); + } + return phoneNumber; + } + public TwilioSmsSender(TwilioSmsProviderConfiguration config) { if (StringUtils.isEmpty(config.getAccountSid()) || StringUtils.isEmpty(config.getAccountToken()) || StringUtils.isEmpty(config.getNumberFrom())) { throw new IllegalArgumentException("Invalid twilio sms provider configuration: accountSid, accountToken and numberFrom should be specified!"); } - this.numberFrom = this.validatePhoneNumber(config.getNumberFrom()); + this.numberFrom = this.validatePhoneTwilioNumber(config.getNumberFrom()); this.twilioRestClient = new TwilioRestClient.Builder(config.getAccountSid(), config.getAccountToken()).build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java index 75577fc5a4..c8a4e80e0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java @@ -52,13 +52,15 @@ import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import org.thingsboard.server.utils.EventDeduplicationExecutor; import javax.annotation.Nullable; import javax.annotation.PostConstruct; @@ -89,7 +91,7 @@ import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @Service @TbCoreComponent @Slf4j -public class DefaultDeviceStateService implements DeviceStateService { +public class DefaultDeviceStateService extends TbApplicationEventListener implements DeviceStateService { public static final String ACTIVITY_STATE = "active"; public static final String LAST_CONNECT_TIME = "lastConnectTime"; @@ -126,13 +128,13 @@ public class DefaultDeviceStateService implements DeviceStateService { @Getter private int initFetchPackSize; - private volatile boolean clusterUpdatePending = false; - private ListeningScheduledExecutorService queueExecutor; private final ConcurrentMap> partitionedDevices = new ConcurrentHashMap<>(); private final ConcurrentMap deviceStates = new ConcurrentHashMap<>(); private final ConcurrentMap deviceLastReportedActivity = new ConcurrentHashMap<>(); private final ConcurrentMap deviceLastSavedActivity = new ConcurrentHashMap<>(); + private volatile EventDeduplicationExecutor> deduplicationExecutor; + public DefaultDeviceStateService(TenantService tenantService, DeviceService deviceService, AttributesService attributesService, TimeseriesService tsService, @@ -155,6 +157,7 @@ public class DefaultDeviceStateService implements DeviceStateService { // Should be always single threaded due to absence of locks. queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-state"))); queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS); + deduplicationExecutor = new EventDeduplicationExecutor<>(DefaultDeviceStateService.class.getSimpleName(), queueExecutor, this::initStateFromDB); } @PreDestroy @@ -204,7 +207,6 @@ public class DefaultDeviceStateService implements DeviceStateService { if (!state.isActive()) { state.setActive(true); save(deviceId, ACTIVITY_STATE, state.isActive()); - stateData.getMetaData().putValue("scope", SERVER_SCOPE); pushRuleEngineMessage(stateData, ACTIVITY_EVENT); } } @@ -292,25 +294,14 @@ public class DefaultDeviceStateService implements DeviceStateService { } } - volatile Set pendingPartitions; - @Override - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { - synchronized (this) { - pendingPartitions = partitionChangeEvent.getPartitions(); - if (!clusterUpdatePending) { - clusterUpdatePending = true; - queueExecutor.submit(() -> { - clusterUpdatePending = false; - initStateFromDB(); - }); - } - } + deduplicationExecutor.submit(partitionChangeEvent.getPartitions()); } } - private void initStateFromDB() { + private void initStateFromDB(Set pendingPartitions) { try { log.info("CURRENT PARTITIONS: {}", partitionedDevices.keySet()); log.info("NEW PARTITIONS: {}", pendingPartitions); @@ -456,7 +447,7 @@ public class DefaultDeviceStateService implements DeviceStateService { } private Function, DeviceStateData> extractDeviceStateData(Device device) { - return new Function, DeviceStateData>() { + return new Function<>() { @Nullable @Override public DeviceStateData apply(@Nullable List data) { @@ -512,7 +503,11 @@ public class DefaultDeviceStateService implements DeviceStateService { } else { data = JacksonUtil.toString(state); } - TbMsg tbMsg = TbMsg.newMsg(msgType, stateData.getDeviceId(), stateData.getMetaData().copy(), TbMsgDataType.JSON, data); + TbMsgMetaData md = stateData.getMetaData().copy(); + if(!persistToTelemetry){ + md.putValue(DataConstants.SCOPE, SERVER_SCOPE); + } + TbMsg tbMsg = TbMsg.newMsg(msgType, stateData.getDeviceId(), md, TbMsgDataType.JSON, data); clusterService.pushMsgToRuleEngine(stateData.getTenantId(), stateData.getDeviceId(), tbMsg, null); } catch (Exception e) { log.warn("[{}] Failed to push inactivity alarm: {}", stateData.getDeviceId(), state, e); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index bafbb45ba8..ce8838aaa8 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -39,7 +39,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos.*; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateProto; @@ -48,6 +48,7 @@ import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -76,7 +77,7 @@ import java.util.function.Predicate; @Slf4j @TbCoreComponent @Service -public class DefaultSubscriptionManagerService implements SubscriptionManagerService { +public class DefaultSubscriptionManagerService extends TbApplicationEventListener implements SubscriptionManagerService { @Autowired private AttributesService attrService; @@ -178,7 +179,7 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer } @Override - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { Set removedPartitions = new HashSet<>(currentPartitions); removedPartitions.removeAll(partitionChangeEvent.getPartitions()); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index 124823a6ad..330b096b78 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -54,6 +54,7 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; @@ -92,7 +93,7 @@ import java.util.stream.Collectors; public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService { private static final int DEFAULT_LIMIT = 100; - private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); + private final Map> subscriptionsBySessionId = new ConcurrentHashMap<>(); @Autowired private TelemetryWebSocketService wsService; @@ -202,7 +203,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc //TODO: validate number of dynamic page links against rate limits. Ignore dynamic flag if limit is reached. TbEntityDataSubCtx finalCtx = ctx; ScheduledFuture task = scheduler.scheduleWithFixedDelay( - () -> refreshDynamicQuery(tenantId, customerId, finalCtx), + () -> refreshDynamicQuery(finalCtx), dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); finalCtx.setRefreshTask(task); } @@ -235,6 +236,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc }, wsCallBackExecutor); } + @Override + public void handleCmd(TelemetryWebSocketSessionRef session, EntityCountCmd cmd) { + TbEntityCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); + if (ctx == null) { + ctx = createSubCtx(session, cmd); + long start = System.currentTimeMillis(); + ctx.fetchData(); + long end = System.currentTimeMillis(); + stats.getRegularQueryInvocationCnt().incrementAndGet(); + stats.getRegularQueryTimeSpent().addAndGet(end - start); + TbEntityCountSubCtx finalCtx = ctx; + ScheduledFuture task = scheduler.scheduleWithFixedDelay( + () -> refreshDynamicQuery(finalCtx), + dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); + finalCtx.setRefreshTask(task); + } else { + log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd); + } + } + @Override public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) { TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); @@ -267,7 +288,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } } - private void refreshDynamicQuery(TenantId tenantId, CustomerId customerId, TbEntityDataSubCtx finalCtx) { + private void refreshDynamicQuery(TbAbstractSubCtx finalCtx) { try { long start = System.currentTimeMillis(); finalCtx.update(); @@ -299,16 +320,30 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { - Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription); - ctx.setAndResolveQuery(cmd.getQuery()); + if (cmd.getQuery() != null) { + ctx.setAndResolveQuery(cmd.getQuery()); + } + sessionSubs.put(cmd.getCmdId(), ctx); + return ctx; + } + + private TbEntityCountSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, + attributesService, stats, sessionRef, cmd.getCmdId()); + if (cmd.getQuery() != null) { + ctx.setAndResolveQuery(cmd.getQuery()); + } sessionSubs.put(cmd.getCmdId(), ctx); return ctx; } + private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { - Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); + Map sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription); ctx.setAndResolveQuery(cmd.getQuery()); @@ -316,8 +351,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc return ctx; } - private T getSubCtx(String sessionId, int cmdId) { - Map sessionSubs = subscriptionsBySessionId.get(sessionId); + @SuppressWarnings("unchecked") + private T getSubCtx(String sessionId, int cmdId) { + Map sessionSubs = subscriptionsBySessionId.get(sessionId); if (sessionSubs != null) { return (T) sessionSubs.get(cmdId); } else { @@ -461,19 +497,18 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc cleanupAndCancel(getSubCtx(sessionId, cmd.getCmdId())); } - private void cleanupAndCancel(TbAbstractDataSubCtx ctx) { + private void cleanupAndCancel(TbAbstractSubCtx ctx) { if (ctx != null) { ctx.cancelTasks(); - ctx.clearEntitySubscriptions(); - ctx.clearDynamicValueSubscriptions(); + ctx.clearSubscriptions(); } } @Override public void cancelAllSessionSubscriptions(String sessionId) { - Map sessionSubs = subscriptionsBySessionId.remove(sessionId); + Map sessionSubs = subscriptionsBySessionId.remove(sessionId); if (sessionSubs != null) { - sessionSubs.values().stream().filter(sub -> sub instanceof TbEntityDataSubCtx).map(sub -> (TbEntityDataSubCtx) sub).forEach(this::cleanupAndCancel); + sessionSubs.values().forEach(this::cleanupAndCancel); } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 868d14ac3e..0220a94964 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -28,6 +28,7 @@ import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.telemetry.sub.AlarmSubscriptionUpdate; @@ -62,6 +63,34 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer private SubscriptionManagerService subscriptionManagerService; private ExecutorService subscriptionUpdateExecutor; + + private TbApplicationEventListener partitionChangeListener = new TbApplicationEventListener<>() { + @Override + protected void onTbApplicationEvent(PartitionChangeEvent event) { + if (ServiceType.TB_CORE.equals(event.getServiceType())) { + currentPartitions.clear(); + currentPartitions.addAll(event.getPartitions()); + } + } + }; + + private TbApplicationEventListener clusterTopologyChangeListener = new TbApplicationEventListener<>() { + @Override + protected void onTbApplicationEvent(ClusterTopologyChangeEvent event) { + if (event.getServiceQueueKeys().stream().anyMatch(key -> ServiceType.TB_CORE.equals(key.getServiceType()))) { + /* + * If the cluster topology has changed, we need to push all current subscriptions to SubscriptionManagerService again. + * Otherwise, the SubscriptionManagerService may "forget" those subscriptions in case of restart. + * Although this is resource consuming operation, it is cheaper than sending ping/pong commands periodically + * It is also cheaper then caching the subscriptions by entity id and then lookup of those caches every time we have new telemetry in SubscriptionManagerService. + * Even if we cache locally the list of active subscriptions by entity id, it is still time consuming operation to get them from cache + * Since number of subscriptions is usually much less then number of devices that are pushing data. + */ + subscriptionsBySessionId.values().forEach(map -> map.values() + .forEach(sub -> pushSubscriptionToManagerService(sub, true))); + } + } + }; @PostConstruct public void initExecutor() { @@ -77,28 +106,14 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer @Override @EventListener(PartitionChangeEvent.class) - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { - if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { - currentPartitions.clear(); - currentPartitions.addAll(partitionChangeEvent.getPartitions()); - } + public void onApplicationEvent(PartitionChangeEvent event) { + partitionChangeListener.onApplicationEvent(event); } @Override @EventListener(ClusterTopologyChangeEvent.class) public void onApplicationEvent(ClusterTopologyChangeEvent event) { - if (event.getServiceQueueKeys().stream().anyMatch(key -> ServiceType.TB_CORE.equals(key.getServiceType()))) { - /* - * If the cluster topology has changed, we need to push all current subscriptions to SubscriptionManagerService again. - * Otherwise, the SubscriptionManagerService may "forget" those subscriptions in case of restart. - * Although this is resource consuming operation, it is cheaper than sending ping/pong commands periodically - * It is also cheaper then caching the subscriptions by entity id and then lookup of those caches every time we have new telemetry in SubscriptionManagerService. - * Even if we cache locally the list of active subscriptions by entity id, it is still time consuming operation to get them from cache - * Since number of subscriptions is usually much less then number of devices that are pushing data. - */ - subscriptionsBySessionId.values().forEach(map -> map.values() - .forEach(sub -> pushSubscriptionToManagerService(sub, true))); - } + clusterTopologyChangeListener.onApplicationEvent(event); } //TODO 3.1: replace null callbacks with callbacks from websocket service. @@ -123,6 +138,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override + @SuppressWarnings("unchecked") public void onSubscriptionUpdate(String sessionId, TelemetrySubscriptionUpdate update, TbCallback callback) { TbSubscription subscription = subscriptionsBySessionId .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); @@ -143,6 +159,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override + @SuppressWarnings("unchecked") public void onSubscriptionUpdate(String sessionId, AlarmSubscriptionUpdate update, TbCallback callback) { TbSubscription subscription = subscriptionsBySessionId .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index 8b0f5b3a94..08f7bfa1a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -15,32 +15,16 @@ */ package org.thingsboard.server.service.subscription; -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.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; -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.id.UserId; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AbstractDataQuery; -import org.thingsboard.server.common.data.query.ComplexFilterPredicate; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.DynamicValueSourceType; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.common.data.query.FilterPredicateType; -import org.thingsboard.server.common.data.query.KeyFilter; -import org.thingsboard.server.common.data.query.KeyFilterPredicate; -import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; @@ -52,140 +36,25 @@ import java.util.ArrayList; import java.util.Arrays; 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.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; import java.util.function.Function; import java.util.stream.Collectors; @Slf4j -@Data -public abstract class TbAbstractDataSubCtx> { +public abstract class TbAbstractDataSubCtx> extends TbAbstractSubCtx { - protected final String serviceId; - protected final SubscriptionServiceStatistics stats; - protected final TelemetryWebSocketService wsService; - protected final EntityService entityService; - protected final TbLocalSubscriptionService localSubscriptionService; - protected final AttributesService attributesService; - protected final TelemetryWebSocketSessionRef sessionRef; - protected final int cmdId; protected final Map subToEntityIdMap; - protected final Set subToDynamicValueKeySet; - @Getter - protected final Map> dynamicValues; @Getter protected PageData data; - @Getter - @Setter - protected T query; - @Setter - protected volatile ScheduledFuture refreshTask; public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) { - this.serviceId = serviceId; - this.wsService = wsService; - this.entityService = entityService; - this.localSubscriptionService = localSubscriptionService; - this.attributesService = attributesService; - this.stats = stats; - this.sessionRef = sessionRef; - this.cmdId = cmdId; + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); this.subToEntityIdMap = new ConcurrentHashMap<>(); - this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet(); - this.dynamicValues = new ConcurrentHashMap<>(); - } - - public void setAndResolveQuery(T query) { - dynamicValues.clear(); - this.query = query; - if (query.getKeyFilters() != null) { - for (KeyFilter filter : query.getKeyFilters()) { - registerDynamicValues(filter.getPredicate()); - } - } - resolve(getTenantId(), getCustomerId(), getUserId()); - } - - public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) { - List> futures = new ArrayList<>(); - for (DynamicValueKey key : dynamicValues.keySet()) { - switch (key.getSourceType()) { - case CURRENT_TENANT: - futures.add(resolveEntityValue(tenantId, tenantId, key)); - break; - case CURRENT_CUSTOMER: - if (customerId != null && !customerId.isNullUid()) { - futures.add(resolveEntityValue(tenantId, customerId, key)); - } - break; - case CURRENT_USER: - if (userId != null && !userId.isNullUid()) { - futures.add(resolveEntityValue(tenantId, userId, key)); - } - break; - } - } - try { - Map> tmpSubMap = new HashMap<>(); - for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) { - tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub); - } - for (EntityId entityId : tmpSubMap.keySet()) { - Map keyStates = new HashMap<>(); - Map dynamicValueKeySubMap = tmpSubMap.get(entityId); - dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs())); - int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); - TbAttributeSubscription sub = TbAttributeSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionRef.getSessionId()) - .subscriptionId(subIdx) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap)) - .allKeys(false) - .keyStates(keyStates) - .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) - .build(); - subToDynamicValueKeySet.add(subIdx); - localSubscriptionService.addSubscription(sub); - } - } catch (InterruptedException | ExecutionException e) { - log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); - } - - } - - private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, - Map dynamicValueKeySubMap) { - Map latestUpdate = new HashMap<>(); - subscriptionUpdate.getData().forEach((k, v) -> { - Object[] data = (Object[]) v.get(0); - latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1])); - }); - - boolean invalidateFilter = false; - for (Map.Entry entry : latestUpdate.entrySet()) { - String k = entry.getKey(); - TsValue tsValue = entry.getValue(); - DynamicValueKeySub sub = dynamicValueKeySubMap.get(k); - if (sub.updateValue(tsValue)) { - invalidateFilter = true; - updateDynamicValuesByKey(sub, tsValue); - } - } - - if (invalidateFilter) { - update(); - } } public void fetchData() { @@ -231,102 +100,10 @@ public abstract class TbAbstractDataSubCtx lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) { - this.lastUpdateTs = value.getTs(); - this.lastUpdateValue = value.getValue(); - return true; - } else { - return false; - } - } - } - - private ListenableFuture resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) { - ListenableFuture> entry = attributesService.find(tenantId, entityId, - TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute()); - return Futures.transform(entry, attributeOpt -> { - DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId); - if (attributeOpt.isPresent()) { - AttributeKvEntry attribute = attributeOpt.get(); - sub.setLastUpdateTs(attribute.getLastUpdateTs()); - sub.setLastUpdateValue(attribute.getValueAsString()); - updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString())); - } - return sub; - }, MoreExecutors.directExecutor()); - } - - private void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) { - DynamicValueKey dvk = sub.getKey(); - switch (dvk.getPredicateType()) { - case STRING: - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue())); - break; - case NUMERIC: - try { - Double dValue = Double.parseDouble(tsValue.getValue()); - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue)); - } catch (NumberFormatException e) { - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null)); - } - break; - case BOOLEAN: - Boolean bValue = Boolean.parseBoolean(tsValue.getValue()); - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue)); - break; - } - } - - private void registerDynamicValues(KeyFilterPredicate predicate) { - switch (predicate.getType()) { - case STRING: - case NUMERIC: - case BOOLEAN: - Optional value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate); - if (value.isPresent()) { - DynamicValue dynamicValue = value.get(); - DynamicValueKey key = new DynamicValueKey( - predicate.getType(), - dynamicValue.getSourceType(), - dynamicValue.getSourceAttribute()); - dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue); - } - break; - case COMPLEX: - ((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues); - } - } - - private Optional> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate predicate) { - if (predicate.getValue().getUserValue() == null) { - return Optional.ofNullable(predicate.getValue().getDynamicValue()); - } else { - return Optional.empty(); - } - } - - public String getSessionId() { - return sessionRef.getSessionId(); - } - - public TenantId getTenantId() { - return sessionRef.getSecurityCtx().getTenantId(); - } - - public CustomerId getCustomerId() { - return sessionRef.getSecurityCtx().getCustomerId(); - } - - public UserId getUserId() { - return sessionRef.getSecurityCtx().getId(); + @Override + public void clearSubscriptions() { + clearEntitySubscriptions(); + super.clearSubscriptions(); } public void clearEntitySubscriptions() { @@ -338,26 +115,6 @@ public abstract class TbAbstractDataSubCtx task) { - this.refreshTask = task; - } - - public void cancelTasks() { - if (this.refreshTask != null) { - log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId); - this.refreshTask.cancel(true); - } - } - public void createSubscriptions(List keys, boolean resultToLatestValues) { Map> keysByType = getEntityKeyByTypeMap(keys); for (EntityData entityData : data.getData()) { @@ -457,14 +214,4 @@ public abstract class TbAbstractDataSubCtx { + + protected final String serviceId; + protected final SubscriptionServiceStatistics stats; + protected final TelemetryWebSocketService wsService; + protected final EntityService entityService; + protected final TbLocalSubscriptionService localSubscriptionService; + protected final AttributesService attributesService; + protected final TelemetryWebSocketSessionRef sessionRef; + protected final int cmdId; + protected final Set subToDynamicValueKeySet; + @Getter + protected final Map> dynamicValues; + @Getter + @Setter + protected T query; + @Setter + protected volatile ScheduledFuture refreshTask; + + public TbAbstractSubCtx(String serviceId, TelemetryWebSocketService wsService, + EntityService entityService, TbLocalSubscriptionService localSubscriptionService, + AttributesService attributesService, SubscriptionServiceStatistics stats, + TelemetryWebSocketSessionRef sessionRef, int cmdId) { + this.serviceId = serviceId; + this.wsService = wsService; + this.entityService = entityService; + this.localSubscriptionService = localSubscriptionService; + this.attributesService = attributesService; + this.stats = stats; + this.sessionRef = sessionRef; + this.cmdId = cmdId; + this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet(); + this.dynamicValues = new ConcurrentHashMap<>(); + } + + public void setAndResolveQuery(T query) { + dynamicValues.clear(); + this.query = query; + if (query != null && query.getKeyFilters() != null) { + for (KeyFilter filter : query.getKeyFilters()) { + registerDynamicValues(filter.getPredicate()); + } + } + resolve(getTenantId(), getCustomerId(), getUserId()); + } + + public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) { + List> futures = new ArrayList<>(); + for (DynamicValueKey key : dynamicValues.keySet()) { + switch (key.getSourceType()) { + case CURRENT_TENANT: + futures.add(resolveEntityValue(tenantId, tenantId, key)); + break; + case CURRENT_CUSTOMER: + if (customerId != null && !customerId.isNullUid()) { + futures.add(resolveEntityValue(tenantId, customerId, key)); + } + break; + case CURRENT_USER: + if (userId != null && !userId.isNullUid()) { + futures.add(resolveEntityValue(tenantId, userId, key)); + } + break; + } + } + try { + Map> tmpSubMap = new HashMap<>(); + for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) { + tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub); + } + for (EntityId entityId : tmpSubMap.keySet()) { + Map keyStates = new HashMap<>(); + Map dynamicValueKeySubMap = tmpSubMap.get(entityId); + dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs())); + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); + TbAttributeSubscription sub = TbAttributeSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionRef.getSessionId()) + .subscriptionId(subIdx) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap)) + .allKeys(false) + .keyStates(keyStates) + .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) + .build(); + subToDynamicValueKeySet.add(subIdx); + localSubscriptionService.addSubscription(sub); + } + } catch (InterruptedException | ExecutionException e) { + log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); + } + + } + + private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, + Map dynamicValueKeySubMap) { + Map latestUpdate = new HashMap<>(); + subscriptionUpdate.getData().forEach((k, v) -> { + Object[] data = (Object[]) v.get(0); + latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1])); + }); + + boolean invalidateFilter = false; + for (Map.Entry entry : latestUpdate.entrySet()) { + String k = entry.getKey(); + TsValue tsValue = entry.getValue(); + DynamicValueKeySub sub = dynamicValueKeySubMap.get(k); + if (sub.updateValue(tsValue)) { + invalidateFilter = true; + updateDynamicValuesByKey(sub, tsValue); + } + } + + if (invalidateFilter) { + update(); + } + } + + public abstract void fetchData(); + + protected abstract void update(); + + public void clearSubscriptions() { + clearDynamicValueSubscriptions(); + } + + @Data + private static class DynamicValueKeySub { + private final DynamicValueKey key; + private final EntityId entityId; + private long lastUpdateTs; + private String lastUpdateValue; + + boolean updateValue(TsValue value) { + if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) { + this.lastUpdateTs = value.getTs(); + this.lastUpdateValue = value.getValue(); + return true; + } else { + return false; + } + } + } + + private ListenableFuture resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) { + ListenableFuture> entry = attributesService.find(tenantId, entityId, + TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute()); + return Futures.transform(entry, attributeOpt -> { + DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId); + if (attributeOpt.isPresent()) { + AttributeKvEntry attribute = attributeOpt.get(); + sub.setLastUpdateTs(attribute.getLastUpdateTs()); + sub.setLastUpdateValue(attribute.getValueAsString()); + updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString())); + } + return sub; + }, MoreExecutors.directExecutor()); + } + + @SuppressWarnings("unchecked") + protected void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) { + DynamicValueKey dvk = sub.getKey(); + switch (dvk.getPredicateType()) { + case STRING: + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue())); + break; + case NUMERIC: + try { + Double dValue = Double.parseDouble(tsValue.getValue()); + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue)); + } catch (NumberFormatException e) { + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null)); + } + break; + case BOOLEAN: + Boolean bValue = Boolean.parseBoolean(tsValue.getValue()); + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue)); + break; + } + } + + @SuppressWarnings("unchecked") + private void registerDynamicValues(KeyFilterPredicate predicate) { + switch (predicate.getType()) { + case STRING: + case NUMERIC: + case BOOLEAN: + Optional value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate); + if (value.isPresent()) { + DynamicValue dynamicValue = value.get(); + DynamicValueKey key = new DynamicValueKey( + predicate.getType(), + dynamicValue.getSourceType(), + dynamicValue.getSourceAttribute()); + dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue); + } + break; + case COMPLEX: + ((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues); + } + } + + private Optional> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate predicate) { + if (predicate.getValue().getUserValue() == null) { + return Optional.ofNullable(predicate.getValue().getDynamicValue()); + } else { + return Optional.empty(); + } + } + + public String getSessionId() { + return sessionRef.getSessionId(); + } + + public TenantId getTenantId() { + return sessionRef.getSecurityCtx().getTenantId(); + } + + public CustomerId getCustomerId() { + return sessionRef.getSecurityCtx().getCustomerId(); + } + + public UserId getUserId() { + return sessionRef.getSecurityCtx().getId(); + } + + protected void clearDynamicValueSubscriptions() { + if (subToDynamicValueKeySet != null) { + for (Integer subId : subToDynamicValueKeySet) { + localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId); + } + subToDynamicValueKeySet.clear(); + } + } + + public void setRefreshTask(ScheduledFuture task) { + this.refreshTask = task; + } + + public void cancelTasks() { + if (this.refreshTask != null) { + log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId); + this.refreshTask.cancel(true); + } + } + + @Data + public static class DynamicValueKey { + @Getter + private final FilterPredicateType predicateType; + @Getter + private final DynamicValueSourceType sourceType; + @Getter + private final String sourceAttribute; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 95f15fcb3f..8af9ba2330 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -90,8 +90,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { AlarmDataUpdate update; if (!entitiesMap.isEmpty()) { long start = System.currentTimeMillis(); - PageData alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(), - query, getOrderedEntityIds()); + PageData alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(), query, getOrderedEntityIds()); long end = System.currentTimeMillis(); stats.getAlarmQueryInvocationCnt().incrementAndGet(); stats.getAlarmQueryTimeSpent().addAndGet(end - start); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java new file mode 100644 index 0000000000..e97892a37d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityCountSubCtx.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2021 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.subscription; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; + +@Slf4j +public class TbEntityCountSubCtx extends TbAbstractSubCtx { + + private volatile int result; + + public TbEntityCountSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService, + TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService, + SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) { + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); + } + + @Override + public void fetchData() { + result = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query); + wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result)); + } + + @Override + protected void update() { + int newCount = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query); + if (newCount != result) { + result = newCount; + wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result)); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java index 60abdeaa9c..8760997bfd 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubscriptionService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.subscription; import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; @@ -25,6 +26,8 @@ public interface TbEntityDataSubscriptionService { void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd); + void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityCountCmd cmd); + void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd); void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index c6da1aad29..da84e8e4ea 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java index 8827a71f70..168e1271fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AbstractSubscriptionService.java @@ -41,6 +41,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.PartitionService; +import org.thingsboard.server.queue.discovery.TbApplicationEventListener; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.subscription.SubscriptionManagerService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; @@ -61,7 +62,7 @@ import java.util.function.Consumer; * Created by ashvayka on 27.03.18. */ @Slf4j -public abstract class AbstractSubscriptionService implements ApplicationListener { +public abstract class AbstractSubscriptionService extends TbApplicationEventListener{ protected final Set currentPartitions = ConcurrentHashMap.newKeySet(); @@ -97,8 +98,7 @@ public abstract class AbstractSubscriptionService implements ApplicationListener } @Override - @EventListener(PartitionChangeEvent.class) - public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { + protected void onTbApplicationEvent(PartitionChangeEvent partitionChangeEvent) { if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { currentPartitions.clear(); currentPartitions.addAll(partitionChangeEvent.getPartitions()); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 495e871903..43bde3ba06 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java @@ -201,6 +201,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer } @Override + public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback) { saveAndNotify(tenantId, entityId, scope, attributes, true, callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java index 2efbc2d0a8..f8f4afd823 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java @@ -51,22 +51,21 @@ import org.thingsboard.server.service.security.ValidationResult; import org.thingsboard.server.service.security.ValidationResultCode; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.subscription.TbAttributeSubscription; +import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; -import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; -import org.thingsboard.server.service.subscription.TbAttributeSubscription; import org.thingsboard.server.service.subscription.TbTimeseriesSubscription; +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; -import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; @@ -89,6 +88,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -151,14 +152,23 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi private ExecutorService executor; private String serviceId; + private ScheduledExecutorService pingExecutor; + @PostConstruct public void initExecutor() { serviceId = serviceInfoProvider.getServiceId(); executor = Executors.newWorkStealingPool(50); + + pingExecutor = Executors.newSingleThreadScheduledExecutor(); + pingExecutor.scheduleWithFixedDelay(this::sendPing, 10000, 10000, TimeUnit.MILLISECONDS); } @PreDestroy public void shutdownExecutor() { + if (pingExecutor != null) { + pingExecutor.shutdownNow(); + } + if (executor != null) { executor.shutdownNow(); } @@ -216,12 +226,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi if (cmdsWrapper.getAlarmDataCmds() != null) { cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd)); } + if (cmdsWrapper.getEntityCountCmds() != null) { + cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd)); + } if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) { cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); } if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) { cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); } + if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) { + cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); + } } } catch (IOException e) { log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); @@ -239,6 +255,16 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } } + private void handleWsEntityCountCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { + String sessionId = sessionRef.getSessionId(); + log.debug("[{}] Processing: {}", sessionId, cmd); + + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) + && validateSubscriptionCmd(sessionRef, cmd)) { + entityDataSubService.handleCmd(sessionRef, cmd); + } + } + private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { String sessionId = sessionRef.getSessionId(); log.debug("[{}] Processing: {}", sessionId, cmd); @@ -264,7 +290,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } @Override - public void sendWsMsg(String sessionId, DataUpdate update) { + public void sendWsMsg(String sessionId, CmdUpdate update) { sendWsMsg(sessionId, update.getCmdId(), update); } @@ -679,6 +705,20 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi return true; } + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) { + if (cmd.getCmdId() < 0) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, + "Cmd id is negative value!"); + sendWsMsg(sessionRef, update); + return false; + } else if (cmd.getQuery() == null) { + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!"); + sendWsMsg(sessionRef, update); + return false; + } + return true; + } + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { if (cmd.getCmdId() < 0) { TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, @@ -744,6 +784,17 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } } + private void sendPing() { + long currentTime = System.currentTimeMillis(); + wsSessionsMap.values().forEach(md -> + executor.submit(() -> { + try { + msgEndpoint.sendPing(md.getSessionRef(), currentTime); + } catch (IOException e) { + log.warn("[{}] Failed to send ping: {}", md.getSessionRef().getSessionId(), e); + } + })); + } private static Optional> getKeys(TelemetryPluginCmd cmd) { if (!StringUtils.isEmpty(cmd.getKeys())) { diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java index 2c132c745c..f10038f116 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java @@ -26,5 +26,7 @@ public interface TelemetryWebSocketMsgEndpoint { void send(TelemetryWebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException; + void sendPing(TelemetryWebSocketSessionRef sessionRef, long currentTime) throws IOException; + void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException; } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java index 6834d26b4d..c8cdbd06af 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.telemetry; +import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; @@ -29,6 +30,6 @@ public interface TelemetryWebSocketService { void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update); - void sendWsMsg(String sessionId, DataUpdate update); + void sendWsMsg(String sessionId, CmdUpdate update); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java index 896babdb6a..1d2de2584d 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/TelemetryPluginCmdsWrapper.java @@ -21,6 +21,8 @@ import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUnsubscribeCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; @@ -46,4 +48,8 @@ public class TelemetryPluginCmdsWrapper { private List alarmDataUnsubscribeCmds; + private List entityCountCmds; + + private List entityCountUnsubscribeCmds; + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java index 5a2ea4ff00..f23484f71f 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AlarmDataUpdate.java @@ -18,14 +18,14 @@ package org.thingsboard.server.service.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.List; +@ToString public class AlarmDataUpdate extends DataUpdate { @Getter @@ -44,8 +44,8 @@ public class AlarmDataUpdate extends DataUpdate { } @Override - public DataUpdateType getDataUpdateType() { - return DataUpdateType.ALARM_DATA; + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ALARM_DATA; } @JsonCreator diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java new file mode 100644 index 0000000000..351dfec15c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdate.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class CmdUpdate { + + private final int cmdId; + private final int errorCode; + private final String errorMsg; + + public abstract CmdUpdateType getCmdUpdateType(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java similarity index 92% rename from application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java rename to application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java index 2a9930e2b1..9df281cbb9 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdateType.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/CmdUpdateType.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.service.telemetry.cmd.v2; -public enum DataUpdateType { +public enum CmdUpdateType { ENTITY_DATA, - ALARM_DATA + ALARM_DATA, + COUNT_DATA } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java index 5540acd782..d0658044d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/DataUpdate.java @@ -15,24 +15,24 @@ */ package org.thingsboard.server.service.telemetry.cmd.v2; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.List; -@Data -@AllArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public abstract class DataUpdate { +public abstract class DataUpdate extends CmdUpdate { - private final int cmdId; + @Getter private final PageData data; + @Getter private final List update; - private final int errorCode; - private final String errorMsg; + + public DataUpdate(int cmdId, PageData data, List update, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.data = data; + this.update = update; + } public DataUpdate(int cmdId, PageData data, List update) { this(cmdId, data, update, SubscriptionErrorCode.NO_ERROR.getCode(), null); @@ -42,5 +42,4 @@ public abstract class DataUpdate { this(cmdId, null, null, errorCode, errorMsg); } - public abstract DataUpdateType getDataUpdateType(); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java new file mode 100644 index 0000000000..d0b7fc8454 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountCmd.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityDataQuery; + +public class EntityCountCmd extends DataCmd { + + @Getter + private final EntityCountQuery query; + + @JsonCreator + public EntityCountCmd(@JsonProperty("cmdId") int cmdId, + @JsonProperty("query") EntityCountQuery query) { + super(cmdId); + this.query = query; + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java new file mode 100644 index 0000000000..c597fa6634 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUnsubscribeCmd.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; + +@Data +public class EntityCountUnsubscribeCmd implements UnsubscribeCmd { + + private final int cmdId; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java new file mode 100644 index 0000000000..6617f8e63c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityCountUpdate.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; + +import java.util.List; + +@ToString +public class EntityCountUpdate extends CmdUpdate { + + @Getter + private int count; + + public EntityCountUpdate(int cmdId, int count) { + super(cmdId, SubscriptionErrorCode.NO_ERROR.getCode(), null); + this.count = count; + } + + public EntityCountUpdate(int cmdId, int errorCode, String errorMsg) { + super(cmdId, errorCode, errorMsg); + } + + @Override + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.COUNT_DATA; + } + + @JsonCreator + public EntityCountUpdate(@JsonProperty("cmdId") int cmdId, + @JsonProperty("count") int count, + @JsonProperty("errorCode") int errorCode, + @JsonProperty("errorMsg") String errorMsg) { + super(cmdId, errorCode, errorMsg); + this.count = count; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java index f08dee8ba3..0af6f85697 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataUpdate.java @@ -16,16 +16,16 @@ package org.thingsboard.server.service.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; +import lombok.ToString; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; import java.util.List; - +@ToString public class EntityDataUpdate extends DataUpdate { @Getter @@ -41,8 +41,8 @@ public class EntityDataUpdate extends DataUpdate { } @Override - public DataUpdateType getDataUpdateType() { - return DataUpdateType.ENTITY_DATA; + public CmdUpdateType getCmdUpdateType() { + return CmdUpdateType.ENTITY_DATA; } @JsonCreator diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java index d34f5c6fdf..5e046ee581 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/UnsubscribeCmd.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.service.telemetry.cmd.v2; -import lombok.Data; - public interface UnsubscribeCmd { int getCmdId(); diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 5808d5ad39..eea7af582c 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -25,6 +25,7 @@ import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; @@ -38,9 +39,12 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -49,41 +53,43 @@ import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.relation.RelationService; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.DeviceCredentialsProto; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetResourcesRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsgOrBuilder; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionResponseStatus; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.queue.TbClusterService; import org.thingsboard.server.service.state.DeviceStateService; +import java.util.Collections; +import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; /** * Created by ashvayka on 05.10.18. @@ -106,6 +112,7 @@ public class DefaultTransportApiService implements TransportApiService { private final TbClusterService tbClusterService; private final DataDecodingEncodingService dataDecodingEncodingService; private final DeviceProvisionService deviceProvisionService; + private final ResourceService resourceService; private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); @@ -114,7 +121,7 @@ public class DefaultTransportApiService implements TransportApiService { RelationService relationService, DeviceCredentialsService deviceCredentialsService, DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService, TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService, - DeviceProvisionService deviceProvisionService) { + DeviceProvisionService deviceProvisionService, ResourceService resourceService) { this.deviceProfileCache = deviceProfileCache; this.tenantProfileCache = tenantProfileCache; this.apiUsageStateService = apiUsageStateService; @@ -126,6 +133,7 @@ public class DefaultTransportApiService implements TransportApiService { this.tbClusterService = tbClusterService; this.dataDecodingEncodingService = dataDecodingEncodingService; this.deviceProvisionService = deviceProvisionService; + this.resourceService = resourceService; } @Override @@ -149,9 +157,19 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasEntityProfileRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getEntityProfileRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasLwM2MRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getLwM2MRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasValidateDeviceLwM2MCredentialsRequestMsg()) { + ValidateDeviceLwM2MCredentialsRequestMsg msg = transportApiRequestMsg.getValidateDeviceLwM2MCredentialsRequestMsg(); + return Futures.transform(validateCredentials(msg.getCredentialsId(), DeviceCredentialsType.LWM2M_CREDENTIALS), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasProvisionDeviceRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getProvisionDeviceRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasResourcesRequestMsg()) { + return Futures.transform(handle(transportApiRequestMsg.getResourcesRequestMsg()), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } return Futures.transform(getEmptyTransportApiResponseFuture(), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); @@ -310,14 +328,15 @@ public class DefaultTransportApiService implements TransportApiService { return TransportApiResponseMsg.newBuilder().setProvisionDeviceResponseMsg(TransportProtos.ProvisionDeviceResponseMsg.newBuilder().setStatus(status).build()).build(); } TransportProtos.ProvisionDeviceResponseMsg.Builder provisionResponse = TransportProtos.ProvisionDeviceResponseMsg.newBuilder() - .setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name())) - .setStatus(status); - switch (deviceCredentials.getCredentialsType()){ + .setCredentialsType(TransportProtos.CredentialsType.valueOf(deviceCredentials.getCredentialsType().name())) + .setStatus(status); + switch (deviceCredentials.getCredentialsType()) { case ACCESS_TOKEN: provisionResponse.setCredentialsValue(deviceCredentials.getCredentialsId()); break; case MQTT_BASIC: case X509_CERTIFICATE: + case LWM2M_CREDENTIALS: provisionResponse.setCredentialsValue(deviceCredentials.getCredentialsValue()); break; } @@ -347,6 +366,40 @@ public class DefaultTransportApiService implements TransportApiService { return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setEntityProfileResponseMsg(builder).build()); } + private ListenableFuture handle(GetResourcesRequestMsg requestMsg) { + TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB())); + TransportProtos.GetResourcesResponseMsg.Builder builder = TransportProtos.GetResourcesResponseMsg.newBuilder(); + String resourceType = requestMsg.getResourceType(); + String resourceId = requestMsg.getResourceId(); + + List resources; + + if (resourceType != null && resourceId != null) { + resources = Collections.singletonList(toProto( + resourceService.getResource(tenantId, ResourceType.valueOf(resourceType), resourceId))); + } else { + //TODO: add page link params to request proto if need or remove this + resources = resourceService.findResourcesByTenantId(tenantId, new PageLink(100)) + .getData() + .stream() + .map(this::toProto) + .collect(Collectors.toList()); + } + + builder.addAllResources(resources); + return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setResourcesResponseMsg(builder).build()); + } + + private TransportProtos.ResourceMsg toProto(Resource resource) { + return TransportProtos.ResourceMsg.newBuilder() + .setTenantIdMSB(resource.getTenantId().getId().getMostSignificantBits()) + .setTenantIdLSB(resource.getTenantId().getId().getLeastSignificantBits()) + .setResourceType(resource.getResourceType().name()) + .setResourceId(resource.getResourceId()) + .setValue(resource.getValue()) + .build(); + } + private ListenableFuture getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) { return Futures.transform(deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, deviceId), device -> { if (device == null) { @@ -396,4 +449,40 @@ public class DefaultTransportApiService implements TransportApiService { return TransportApiResponseMsg.newBuilder() .setValidateCredResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); } + + private ListenableFuture handle(TransportProtos.LwM2MRequestMsg requestMsg) { + if (requestMsg.hasRegistrationMsg()) { + return handleRegistration(requestMsg.getRegistrationMsg()); + } else { + return Futures.immediateFailedFuture(new RuntimeException("Not supported!")); + } + } + + private ListenableFuture handleRegistration(TransportProtos.LwM2MRegistrationRequestMsg msg) { + TenantId tenantId = new TenantId(UUID.fromString(msg.getTenantId())); + String deviceName = msg.getEndpoint(); + Lock deviceCreationLock = deviceCreationLocks.computeIfAbsent(deviceName, id -> new ReentrantLock()); + deviceCreationLock.lock(); + try { + Device device = deviceService.findDeviceByTenantIdAndName(tenantId, deviceName); + if (device == null) { + device = new Device(); + device.setTenantId(tenantId); + device.setName(deviceName); + device.setType("LwM2M"); + device = deviceService.saveDevice(device); + deviceStateService.onDeviceAdded(device); + } + TransportProtos.LwM2MRegistrationResponseMsg registrationResponseMsg = + TransportProtos.LwM2MRegistrationResponseMsg.newBuilder() + .setDeviceInfo(getDeviceInfoProto(device)).build(); + TransportProtos.LwM2MResponseMsg responseMsg = TransportProtos.LwM2MResponseMsg.newBuilder().setRegistrationMsg(registrationResponseMsg).build(); + return Futures.immediateFuture(TransportApiResponseMsg.newBuilder().setLwM2MResponseMsg(responseMsg).build()); + } catch (JsonProcessingException e) { + log.warn("[{}][{}] Failed to lookup device by gateway id and name", tenantId, deviceName, e); + throw new RuntimeException(e); + } finally { + deviceCreationLock.unlock(); + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java index d542bdbe61..74d81c0307 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/msg/TransportToDeviceActorMsgWrapper.java @@ -34,6 +34,8 @@ import java.util.UUID; @Data public class TransportToDeviceActorMsgWrapper implements TbActorMsg, DeviceAwareMsg, TenantAwareMsg, Serializable { + private static final long serialVersionUID = 7191333353202935941L; + private final TenantId tenantId; private final DeviceId deviceId; private final TransportToDeviceActorMsg msg; diff --git a/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java b/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java new file mode 100644 index 0000000000..4ce0d2f3d5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2021 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.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; + +/** + * This class deduplicate executions of the specified function. + * Useful in cluster mode, when you get event about partition change multiple times. + * Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and + * later, once the processing of first event is done, process last pending task. + * + * @param

parameters of the function + */ +@Slf4j +public class EventDeduplicationExecutor

{ + private final String name; + private final ExecutorService executor; + private final Consumer

function; + private P pendingTask; + private boolean busy; + + public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer

function) { + this.name = name; + this.executor = executor; + this.function = function; + } + + public void submit(P params) { + log.info("[{}] Going to submit: {}", name, params); + synchronized (EventDeduplicationExecutor.this) { + if (!busy) { + busy = true; + pendingTask = null; + try { + log.info("[{}] Submitting task: {}", name, params); + executor.submit(() -> { + try { + log.info("[{}] Executing task: {}", name, params); + function.accept(params); + } catch (Throwable e) { + log.warn("[{}] Failed to process task with parameters: {}", name, params, e); + throw e; + } finally { + unlockAndProcessIfAny(); + } + }); + } catch (Throwable e) { + log.warn("[{}] Failed to submit task with parameters: {}", name, params, e); + unlockAndProcessIfAny(); + throw e; + } + } else { + log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params); + pendingTask = params; + } + } + } + + private void unlockAndProcessIfAny() { + synchronized (EventDeduplicationExecutor.this) { + busy = false; + if (pendingTask != null) { + submit(pendingTask); + } + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java index 6ca82aa062..8ee5a5ebc7 100644 --- a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java @@ -33,6 +33,7 @@ public class MiscUtils { return "The " + propertyName + " property need to be set!"; } + @SuppressWarnings("deprecation") public static HashFunction forName(String name) { switch (name) { case "murmur3_32": diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt index 791f878366..111465dd8a 100644 --- a/application/src/main/resources/banner.txt +++ b/application/src/main/resources/banner.txt @@ -1,3 +1,10 @@ + ______ __ _ __ __ + /_ __/ / /_ (_) ____ ____ _ _____ / /_ ____ ____ _ _____ ____/ / + / / / __ \ / / / __ \ / __ `/ / ___/ / __ \ / __ \ / __ `/ / ___/ / __ / + / / / / / / / / / / / / / /_/ / (__ ) / /_/ // /_/ // /_/ / / / / /_/ / +/_/ /_/ /_/ /_/ /_/ /_/ \__, / /____/ /_.___/ \____/ \__,_/ /_/ \__,_/ + /____/ + =================================================== :: ${application.title} :: ${application.formatted-version} =================================================== diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 6d10a74854..f8085ec985 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -30,10 +30,13 @@ - + + + + @@ -41,4 +44,4 @@ - \ No newline at end of file + diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 437a672c54..3f7ef449ed 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -34,6 +34,7 @@ server: log_controller_error_stack_trace: "${HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE:false}" ws: send_timeout: "${TB_SERVER_WS_SEND_TIMEOUT:5000}" + ping_timeout: "${TB_SERVER_WS_PING_TIMEOUT:30000}" limits: # Limit the amount of sessions and subscriptions available on each server. Put values to zero to disable particular limitation max_sessions_per_tenant: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_TENANT:0}" @@ -118,6 +119,15 @@ security: githubMapper: emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}" +# Usage statistics parameters +usage: + stats: + report: + enabled: "${USAGE_STATS_REPORT_ENABLED:true}" + interval: "${USAGE_STATS_REPORT_INTERVAL:10}" + check: + cycle: "${USAGE_STATS_CHECK_CYCLE:60000}" + # Dashboard parameters dashboard: # Maximum allowed datapoints fetched by widgets @@ -313,6 +323,9 @@ actors: cache: # caffeine or redis type: "${CACHE_TYPE:caffeine}" + attributes: + # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' that you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random' + enabled: "${CACHE_ATTRIBUTES_ENABLED:true}" caffeine: specs: @@ -346,6 +359,9 @@ caffeine: deviceProfiles: timeToLiveInMinutes: 1440 maxSize: 0 + attributes: + timeToLiveInMinutes: 1440 + maxSize: 100000 redis: # standalone or cluster @@ -489,7 +505,7 @@ js: # Built-in JVM JavaScript environment properties local: # Use Sandboxed (secured) JVM JavaScript environment - use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:true}" + use_js_sandbox: "${USE_LOCAL_JS_SANDBOX:false}" # Specify thread pool size for JavaScript sandbox resource monitor monitor_thread_pool_size: "${LOCAL_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" # Maximum CPU time in milliseconds allowed for script execution @@ -530,6 +546,7 @@ transport: http: enabled: "${HTTP_ENABLED:true}" request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" + max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" # Local MQTT transport parameters mqtt: # Enable/disable mqtt transport protocol. @@ -566,6 +583,68 @@ transport: bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" timeout: "${COAP_TIMEOUT:10000}" + # Local LwM2M transport parameters + lwm2m: + # Enable/disable lvm2m transport protocol. + enabled: "${LWM2M_ENABLED:true}" + # We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to + # send a Confirmable message to the time when an acknowledgement is no longer expected. + # DEFAULT_TIMEOUT = 2 * 60 * 1000l; 2 min in ms + timeout: "${LWM2M_TIMEOUT:120000}" +# model_path_file: "${LWM2M_MODEL_PATH_FILE:./common/transport/lwm2m/src/main/resources/models/}" + model_path_file: "${LWM2M_MODEL_PATH_FILE:}" + recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" + recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" + request_pool_size: "${LWM2M_REQUEST_POOL_SIZE:100}" + request_error_pool_size: "${LWM2M_REQUEST_ERROR_POOL_SIZE:10}" + registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" + update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" + un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" + secure: + # Certificate_x509: + # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format + # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh + key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" + # key_store_type: "${LWM2M_KEYSTORE_TYPE:PKCS12}" +# key_store_path_file: "${KEY_STORE_PATH_FILE:/usr/share/thingsboard/conf/credentials/serverKeyStore.jks}" + key_store_path_file: "${KEY_STORE_PATH_FILE:}" + key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}" + root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" + enable_gen_new_key_psk_rpk: "${ENABLE_GEN_NEW_KEY_PSK_RPK:false}" + server: + id: "${LWM2M_SERVER_ID:123}" + bind_address: "${LWM2M_BIND_ADDRESS:0.0.0.0}" + bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC:5685}" + secure: + bind_address_security: "${LWM2M_BIND_ADDRESS_SECURITY:0.0.0.0}" + bind_port_security: "${LWM2M_BIND_PORT_SECURITY:5686}" + # create_rpk: "${CREATE_RPK:}" + # Only for RPK: Public & Private Key. If the keystore file is missing or not working + # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d0301070342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b] + # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b], + # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] + public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}" + public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" + bootstrap: + enable: "${LWM2M_BOOTSTRAP_ENABLED:true}" + id: "${LWM2M_SERVER_ID:111}" + bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" + bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}" + secure: + bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" + bind_port_security: "${LWM2M_BIND_PORT_SEC_BS:5688}" + # Only for RPK: Public & Private Key. If the keystore file is missing or not working + # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] + # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34] + # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34], + public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" + public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_BOOTSTRAP:bootstrap}" + # Use redis for Security and Registration stores + redis.enabled: "${LWM2M_REDIS_ENABLED:false}" snmp: enabled: "${SNMP_ENABLED:true}" @@ -615,6 +694,10 @@ queue: transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" + consumer-stats: + enabled: "${TB_QUEUE_KAFKA_CONSUMER_STATS_ENABLED:true}" + print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" + kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" aws_sqs: use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" 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 12f4e5fced..e25592e35e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -375,6 +375,10 @@ public abstract class AbstractWebTest { return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); } + protected T doGetAsyncTyped(String urlTemplate, TypeReference responseType, Object... urlVariables) throws Exception { + return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); + } + protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception { MockHttpServletRequestBuilder getRequest; getRequest = get(urlTemplate, urlVariables); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java index d4e4b4e0d6..5d234d2691 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityQueryControllerTest.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.NumericFilterPredicate; @@ -132,6 +133,14 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe count = doPostWithResponse("/api/entitiesQuery/count", countQuery, Long.class); Assert.assertEquals(97, count.longValue()); + + EntityTypeFilter filter2 = new EntityTypeFilter(); + filter2.setEntityType(EntityType.DEVICE); + + EntityCountQuery countQuery2 = new EntityCountQuery(filter2); + + Long count2 = doPostWithResponse("/api/entitiesQuery/count", countQuery2, Long.class); + Assert.assertEquals(97, count2.longValue()); } @Test @@ -198,11 +207,31 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe Assert.assertEquals(11, data.getTotalElements()); Assert.assertEquals("Device19", data.getData().get(0).getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()); + + EntityTypeFilter filter2 = new EntityTypeFilter(); + filter2.setEntityType(EntityType.DEVICE); + + EntityDataSortOrder sortOrder2 = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink2 = new EntityDataPageLink(10, 0, null, sortOrder2); + List entityFields2 = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + EntityDataQuery query2 = new EntityDataQuery(filter2, pageLink2, entityFields2, null, null); + + PageData data2 = + doPostWithTypedResponse("/api/entitiesQuery/find", query2, new TypeReference>() { + }); + + Assert.assertEquals(97, data2.getTotalElements()); + Assert.assertEquals(10, data2.getTotalPages()); + Assert.assertTrue(data2.hasNext()); + Assert.assertEquals(10, data2.getData().size()); + } @Test public void testFindEntityDataByQueryWithAttributes() throws Exception { - List devices = new ArrayList<>(); List temperatures = new ArrayList<>(); List highTemperatures = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index 35ff3703c0..c6d0073d83 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -347,8 +347,8 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes Thread.sleep(1000); - List> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + - "/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class); + List> values = doGetAsyncTyped("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + + "/values/attributes?keys=" + String.join(",", actualAttributesSet), new TypeReference<>() {}); assertEquals("value1", getValue(values, "caKey1")); assertEquals(true, getValue(values, "caKey2")); @@ -364,8 +364,8 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes Set expectedActualAttributesSet = new HashSet<>(Arrays.asList("caKey1", "caKey2", "caKey3", "caKey4")); assertTrue(actualAttributesSet.containsAll(expectedActualAttributesSet)); - List> valueTelemetryOfDevices = doGetAsync("/api/plugins/telemetry/DEVICE/" + testDevice.getId().getId().toString() + - "/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class); + List> valueTelemetryOfDevices = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + testDevice.getId().getId().toString() + + "/values/attributes?keys=" + String.join(",", actualAttributesSet), new TypeReference<>() {}); EntityView view = new EntityView(); view.setEntityId(testDevice.getId()); @@ -379,8 +379,8 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes Thread.sleep(1000); - List> values = doGetAsync("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + - "/values/attributes?keys=" + String.join(",", actualAttributesSet), List.class); + List> values = doGetAsyncTyped("/api/plugins/telemetry/ENTITY_VIEW/" + savedView.getId().getId().toString() + + "/values/attributes?keys=" + String.join(",", actualAttributesSet), new TypeReference<>() {}); assertEquals(0, values.size()); } @@ -449,12 +449,12 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes } private Set getTelemetryKeys(String type, String id) throws Exception { - return new HashSet<>(doGetAsync("/api/plugins/telemetry/" + type + "/" + id + "/keys/timeseries", List.class)); + return new HashSet<>(doGetAsyncTyped("/api/plugins/telemetry/" + type + "/" + id + "/keys/timeseries", new TypeReference<>() {})); } private Map>> getTelemetryValues(String type, String id, Set keys, Long startTs, Long endTs) throws Exception { - return doGetAsync("/api/plugins/telemetry/" + type + "/" + id + - "/values/timeseries?keys=" + String.join(",", keys) + "&startTs=" + startTs + "&endTs=" + endTs, Map.class); + return doGetAsyncTyped("/api/plugins/telemetry/" + type + "/" + id + + "/values/timeseries?keys=" + String.join(",", keys) + "&startTs=" + startTs + "&endTs=" + endTs, new TypeReference<>() {}); } private Set getAttributesByKeys(String stringKV) throws Exception { @@ -479,7 +479,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes client.publish("v1/devices/me/attributes", message); Thread.sleep(1000); client.disconnect(); - return new HashSet<>(doGetAsync("/api/plugins/telemetry/DEVICE/" + viewDeviceId + "/keys/attributes", List.class)); + return new HashSet<>(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + viewDeviceId + "/keys/attributes", new TypeReference<>() {})); } private Object getValue(List> values, String stringValue) { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index 299413d9c9..2249940c48 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -35,16 +35,23 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; @@ -243,6 +250,98 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { Assert.assertEquals(new TsValue(dataPoint4.getTs(), dataPoint4.getValueAsString()), tsValues[0]); } + @Test + public void testEntityCountWsCmd() throws Exception { + Device device = new Device(); + device.setName("Device"); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + + AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 42L)); + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Collections.singletonList(dataPoint1)); + + DeviceTypeFilter dtf1 = new DeviceTypeFilter(); + dtf1.setDeviceNameFilter("D"); + dtf1.setDeviceType("default"); + EntityCountQuery edq1 = new EntityCountQuery(dtf1, Collections.emptyList()); + + EntityCountCmd cmd1 = new EntityCountCmd(1, edq1); + + TelemetryPluginCmdsWrapper wrapper1 = new TelemetryPluginCmdsWrapper(); + wrapper1.setEntityCountCmds(Collections.singletonList(cmd1)); + + wsClient.send(mapper.writeValueAsString(wrapper1)); + String msg1 = wsClient.waitForReply(); + EntityCountUpdate update1 = mapper.readValue(msg1, EntityCountUpdate.class); + Assert.assertEquals(1, update1.getCmdId()); + Assert.assertEquals(1, update1.getCount()); + + DeviceTypeFilter dtf2 = new DeviceTypeFilter(); + dtf2.setDeviceNameFilter("D"); + dtf2.setDeviceType("non-existing-device-type"); + EntityCountQuery edq2 = new EntityCountQuery(dtf2, Collections.emptyList()); + + EntityCountCmd cmd2 = new EntityCountCmd(2, edq2); + + TelemetryPluginCmdsWrapper wrapper2 = new TelemetryPluginCmdsWrapper(); + wrapper2.setEntityCountCmds(Collections.singletonList(cmd2)); + wsClient.send(mapper.writeValueAsString(wrapper2)); + + String msg2 = wsClient.waitForReply(); + EntityCountUpdate update2 = mapper.readValue(msg2, EntityCountUpdate.class); + Assert.assertEquals(2, update2.getCmdId()); + Assert.assertEquals(0, update2.getCount()); + + KeyFilter highTemperatureFilter = new KeyFilter(); + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate = new NumericFilterPredicate(); + predicate.setValue(FilterPredicateValue.fromDouble(40)); + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter.setPredicate(predicate); + highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + + DeviceTypeFilter dtf3 = new DeviceTypeFilter(); + dtf3.setDeviceNameFilter("D"); + dtf3.setDeviceType("default"); + EntityCountQuery edq3 = new EntityCountQuery(dtf3, Collections.singletonList(highTemperatureFilter)); + + EntityCountCmd cmd3 = new EntityCountCmd(3, edq3); + + TelemetryPluginCmdsWrapper wrapper3 = new TelemetryPluginCmdsWrapper(); + wrapper3.setEntityCountCmds(Collections.singletonList(cmd3)); + wsClient.send(mapper.writeValueAsString(wrapper3)); + + String msg3 = wsClient.waitForReply(); + EntityCountUpdate update3 = mapper.readValue(msg3, EntityCountUpdate.class); + Assert.assertEquals(3, update3.getCmdId()); + Assert.assertEquals(1, update3.getCount()); + + KeyFilter highTemperatureFilter2 = new KeyFilter(); + highTemperatureFilter2.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + NumericFilterPredicate predicate2 = new NumericFilterPredicate(); + predicate2.setValue(FilterPredicateValue.fromDouble(50)); + predicate2.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperatureFilter2.setPredicate(predicate2); + highTemperatureFilter2.setValueType(EntityKeyValueType.NUMERIC); + + DeviceTypeFilter dtf4 = new DeviceTypeFilter(); + dtf4.setDeviceNameFilter("D"); + dtf4.setDeviceType("default"); + EntityCountQuery edq4 = new EntityCountQuery(dtf4, Collections.singletonList(highTemperatureFilter2)); + + EntityCountCmd cmd4 = new EntityCountCmd(4, edq4); + + TelemetryPluginCmdsWrapper wrapper4 = new TelemetryPluginCmdsWrapper(); + wrapper4.setEntityCountCmds(Collections.singletonList(cmd4)); + wsClient.send(mapper.writeValueAsString(wrapper4)); + + String msg4 = wsClient.waitForReply(); + EntityCountUpdate update4 = mapper.readValue(msg4, EntityCountUpdate.class); + Assert.assertEquals(4, update4.getCmdId()); + Assert.assertEquals(0, update4.getCount()); + } + @Test public void testEntityDataLatestWidgetFlow() throws Exception { Device device = new Device(); diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java index 3186765130..0f969a848f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java @@ -27,7 +27,7 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ // "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", -// "org.thingsboard.server.controller.sql.TenantProfileControllerSqlTest", +// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest", "org.thingsboard.server.controller.sql.*Test", }) public class ControllerSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java index 002474c59d..6ad5db9aff 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java @@ -26,7 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.device.profile.MqttTopics; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest; import java.nio.charset.StandardCharsets; diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java index 7045e28f62..9810c4fe3a 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java @@ -25,7 +25,7 @@ import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.MqttTopics; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest; import java.nio.charset.StandardCharsets; diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java index 0fce47fc84..1011c86b7f 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java @@ -37,7 +37,7 @@ import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; import java.util.concurrent.CountDownLatch; diff --git a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java index 0478a402d9..476b4add4c 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos.CredentialsDataProto; import org.thingsboard.server.gen.transport.TransportProtos.CredentialsType; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceCredentialsMsg; @@ -47,7 +47,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenR import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; -import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 4ae0072029..19e74b6fdc 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -30,7 +30,7 @@ import org.junit.Assert; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.MqttTopics; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest; import java.util.Arrays; diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java index d048546323..874d55606d 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.mqtt.telemetry.attributes; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.junit.After; @@ -80,7 +81,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt List actualKeys = null; while (start <= end) { - actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", List.class); + actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {}); if (actualKeys.size() == expectedKeys.size()) { break; } @@ -96,7 +97,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertEquals(expectedKeySet, actualKeySet); String getAttributesValuesUrl = getAttributesValuesUrl(deviceId, actualKeySet); - List> values = doGetAsync(getAttributesValuesUrl, List.class); + List> values = doGetAsyncTyped(getAttributesValuesUrl, new TypeReference<>() {}); assertAttributesValues(values, expectedKeySet); String deleteAttributesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); doDelete(deleteAttributesUrl); @@ -121,10 +122,10 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt Thread.sleep(2000); - List firstDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/attributes/CLIENT_SCOPE", List.class); + List firstDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {}); Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); - List secondDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/attributes/CLIENT_SCOPE", List.class); + List secondDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() {}); Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); Set expectedKeySet = new HashSet<>(expectedKeys); @@ -135,14 +136,15 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt String getAttributesValuesUrlFirstDevice = getAttributesValuesUrl(firstDevice.getId(), firstDeviceActualKeySet); String getAttributesValuesUrlSecondDevice = getAttributesValuesUrl(firstDevice.getId(), secondDeviceActualKeySet); - List> firstDeviceValues = doGetAsync(getAttributesValuesUrlFirstDevice, List.class); - List> secondDeviceValues = doGetAsync(getAttributesValuesUrlSecondDevice, List.class); + List> firstDeviceValues = doGetAsyncTyped(getAttributesValuesUrlFirstDevice, new TypeReference<>() {}); + List> secondDeviceValues = doGetAsyncTyped(getAttributesValuesUrlSecondDevice, new TypeReference<>() {}); assertAttributesValues(firstDeviceValues, expectedKeySet); assertAttributesValues(secondDeviceValues, expectedKeySet); } + @SuppressWarnings("unchecked") protected void assertAttributesValues(List> deviceValues, Set expectedKeySet) throws JsonProcessingException { for (Map map : deviceValues) { String key = (String) map.get("key"); diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java index 9984c9faae..9ecc3415c2 100644 --- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.mqtt.telemetry.timeseries; +import com.fasterxml.jackson.core.type.TypeReference; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; @@ -25,6 +26,7 @@ import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.device.profile.MqttTopics; @@ -107,7 +109,7 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt List actualKeys = null; while (start <= end) { - actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", List.class); + actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", new TypeReference<>() {}); if (actualKeys.size() == expectedKeys.size()) { break; } @@ -129,13 +131,13 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt } start = System.currentTimeMillis(); end = System.currentTimeMillis() + 5000; - Map>> values = null; + Map>> values = null; while (start <= end) { - values = doGetAsync(getTelemetryValuesUrl, Map.class); + values = doGetAsyncTyped(getTelemetryValuesUrl, new TypeReference<>() {}); boolean valid = values.size() == expectedKeys.size(); if (valid) { for (String key : expectedKeys) { - List> tsValues = values.get(key); + List> tsValues = values.get(key); if (tsValues != null && tsValues.size() > 0) { Object ts = tsValues.get(0).get("ts"); if (ts == null) { @@ -181,10 +183,10 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt Thread.sleep(2000); - List firstDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/timeseries", List.class); + List firstDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + firstDevice.getId() + "/keys/timeseries", new TypeReference<>() {}); Set firstDeviceActualKeySet = new HashSet<>(firstDeviceActualKeys); - List secondDeviceActualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/timeseries", List.class); + List secondDeviceActualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + secondDevice.getId() + "/keys/timeseries", new TypeReference<>() {}); Set secondDeviceActualKeySet = new HashSet<>(secondDeviceActualKeys); Set expectedKeySet = new HashSet<>(expectedKeys); @@ -195,8 +197,8 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt String getTelemetryValuesUrlFirstDevice = getTelemetryValuesUrl(firstDevice.getId(), firstDeviceActualKeySet); String getTelemetryValuesUrlSecondDevice = getTelemetryValuesUrl(firstDevice.getId(), secondDeviceActualKeySet); - Map>> firstDeviceValues = doGetAsync(getTelemetryValuesUrlFirstDevice, Map.class); - Map>> secondDeviceValues = doGetAsync(getTelemetryValuesUrlSecondDevice, Map.class); + Map>> firstDeviceValues = doGetAsyncTyped(getTelemetryValuesUrlFirstDevice, new TypeReference<>() {}); + Map>> secondDeviceValues = doGetAsyncTyped(getTelemetryValuesUrlSecondDevice, new TypeReference<>() {}); assertGatewayDeviceData(firstDeviceValues, expectedKeys); assertGatewayDeviceData(secondDeviceValues, expectedKeys); @@ -212,7 +214,7 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt return "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?startTs=0&endTs=25000&keys=" + String.join(",", actualKeySet); } - private void assertGatewayDeviceData(Map>> deviceValues, List expectedKeys) { + private void assertGatewayDeviceData(Map>> deviceValues, List expectedKeys) { assertEquals(2, deviceValues.get(expectedKeys.get(0)).size()); assertEquals(2, deviceValues.get(expectedKeys.get(1)).size()); @@ -228,11 +230,11 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt } - private void assertValues(Map>> deviceValues, int arrayIndex) { - for (Map.Entry>> entry : deviceValues.entrySet()) { + private void assertValues(Map>> deviceValues, int arrayIndex) { + for (Map.Entry>> entry : deviceValues.entrySet()) { String key = entry.getKey(); - List> tsKv = entry.getValue(); - String value = tsKv.get(arrayIndex).get("value"); + List> tsKv = entry.getValue(); + String value = (String) tsKv.get(arrayIndex).get("value"); switch (key) { case "key1": assertEquals("value1", value); @@ -253,7 +255,7 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt } } - private void assertTs(Map>> deviceValues, List expectedKeys, int ts, int arrayIndex) { + private void assertTs(Map>> deviceValues, List expectedKeys, int ts, int arrayIndex) { assertEquals(ts, deviceValues.get(expectedKeys.get(0)).get(arrayIndex).get("ts")); assertEquals(ts, deviceValues.get(expectedKeys.get(1)).get(arrayIndex).get("ts")); assertEquals(ts, deviceValues.get(expectedKeys.get(2)).get(arrayIndex).get("ts")); diff --git a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java index 7ce23368a9..88d7796af5 100644 --- a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java @@ -21,12 +21,11 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.queue.discovery.HashPartitionService; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; diff --git a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java index 901fc27bb1..6c4e4a9426 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContextTest.java @@ -20,7 +20,7 @@ import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy; diff --git a/application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java b/application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java new file mode 100644 index 0000000000..ac0b2342ec --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java @@ -0,0 +1,155 @@ +/** + * Copyright © 2016-2021 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.util; + +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.server.utils.EventDeduplicationExecutor; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +@Slf4j +@RunWith(MockitoJUnitRunner.class) +public class EventDeduplicationExecutorTest { + + @Test + public void testSimpleFlowSameThread() throws InterruptedException { + simpleFlow(MoreExecutors.newDirectExecutorService()); + } + + @Test + public void testPeriodicFlowSameThread() throws InterruptedException { + periodicFlow(MoreExecutors.newDirectExecutorService()); + } + + @Test + public void testExceptionFlowSameThread() throws InterruptedException { + exceptionFlow(MoreExecutors.newDirectExecutorService()); + } + + @Test + public void testSimpleFlowSingleThread() throws InterruptedException { + simpleFlow(Executors.newSingleThreadExecutor()); + } + + @Test + public void testPeriodicFlowSingleThread() throws InterruptedException { + periodicFlow(Executors.newSingleThreadExecutor()); + } + + @Test + public void testExceptionFlowSingleThread() throws InterruptedException { + exceptionFlow(Executors.newSingleThreadExecutor()); + } + + @Test + public void testSimpleFlowMultiThread() throws InterruptedException { + simpleFlow(Executors.newFixedThreadPool(3)); + } + + @Test + public void testPeriodicFlowMultiThread() throws InterruptedException { + periodicFlow(Executors.newFixedThreadPool(3)); + } + + @Test + public void testExceptionFlowMultiThread() throws InterruptedException { + exceptionFlow(Executors.newFixedThreadPool(3)); + } + + private void simpleFlow(ExecutorService executorService) throws InterruptedException { + try { + Consumer function = Mockito.spy(StringConsumer.class); + EventDeduplicationExecutor executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function); + + String params1 = "params1"; + String params2 = "params2"; + String params3 = "params3"; + + executor.submit(params1); + executor.submit(params2); + executor.submit(params3); + Thread.sleep(500); + Mockito.verify(function).accept(params1); + Mockito.verify(function).accept(params3); + } finally { + executorService.shutdownNow(); + } + } + + private void periodicFlow(ExecutorService executorService) throws InterruptedException { + try { + Consumer function = Mockito.spy(StringConsumer.class); + EventDeduplicationExecutor executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function); + + String params1 = "params1"; + String params2 = "params2"; + String params3 = "params3"; + + executor.submit(params1); + Thread.sleep(500); + executor.submit(params2); + Thread.sleep(500); + executor.submit(params3); + Thread.sleep(500); + Mockito.verify(function).accept(params1); + Mockito.verify(function).accept(params2); + Mockito.verify(function).accept(params3); + } finally { + executorService.shutdownNow(); + } + } + + private void exceptionFlow(ExecutorService executorService) throws InterruptedException { + try { + Consumer function = Mockito.spy(StringConsumer.class); + EventDeduplicationExecutor executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function); + + String params1 = "params1"; + String params2 = "params2"; + String params3 = "params3"; + + Mockito.doThrow(new RuntimeException()).when(function).accept("params1"); + executor.submit(params1); + executor.submit(params2); + Thread.sleep(500); + executor.submit(params3); + Thread.sleep(500); + Mockito.verify(function).accept(params2); + Mockito.verify(function).accept(params3); + } finally { + executorService.shutdownNow(); + } + } + + public static class StringConsumer implements Consumer { + @Override + public void accept(String s) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + +} diff --git a/common/actor/pom.xml b/common/actor/pom.xml index a421b8cef4..1f4849cb79 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -67,7 +67,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActor.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActor.java index 4acadaff2c..0d2ec046a0 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActor.java +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActor.java @@ -30,7 +30,7 @@ public interface TbActor { } default InitFailureStrategy onInitFailure(int attempt, Throwable t) { - return InitFailureStrategy.retryWithDelay(5000 * attempt); + return InitFailureStrategy.retryWithDelay(5000L * attempt); } default ProcessFailureStrategy onProcessFailure(Throwable t) { diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorException.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorException.java index 66ccb3f81c..86329cc641 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorException.java +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorException.java @@ -17,6 +17,8 @@ package org.thingsboard.server.actors; public class TbActorException extends Exception { + private static final long serialVersionUID = 8209771144711980882L; + public TbActorException(String message, Throwable cause) { super(message, cause); } diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java index 225b9351ad..25dd8694f8 100644 --- a/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbActorMailbox.java @@ -17,7 +17,9 @@ package org.thingsboard.server.actors; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.TbActorStopReason; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; @@ -49,6 +51,7 @@ public final class TbActorMailbox implements TbActorCtx { private final AtomicBoolean busy = new AtomicBoolean(FREE); private final AtomicBoolean ready = new AtomicBoolean(NOT_READY); private final AtomicBoolean destroyInProgress = new AtomicBoolean(); + private volatile TbActorStopReason stopReason; public void initActor() { dispatcher.getExecutor().execute(() -> tryInit(1)); @@ -70,7 +73,8 @@ public final class TbActorMailbox implements TbActorCtx { InitFailureStrategy strategy = actor.onInitFailure(attempt, t); if (strategy.isStop() || (settings.getMaxActorInitAttempts() > 0 && attemptIdx > settings.getMaxActorInitAttempts())) { log.info("[{}] Failed to init actor, attempt {}, going to stop attempts.", selfId, attempt, t); - system.stop(selfId); + stopReason = TbActorStopReason.INIT_FAILED; + destroy(); } else if (strategy.getRetryDelay() > 0) { log.info("[{}] Failed to init actor, attempt {}, going to retry in attempts in {}ms", selfId, attempt, strategy.getRetryDelay()); log.debug("[{}] Error", selfId, t); @@ -84,12 +88,28 @@ public final class TbActorMailbox implements TbActorCtx { } private void enqueue(TbActorMsg msg, boolean highPriority) { - if (highPriority) { - highPriorityMsgs.add(msg); + if (!destroyInProgress.get()) { + if (highPriority) { + highPriorityMsgs.add(msg); + } else { + normalPriorityMsgs.add(msg); + } + tryProcessQueue(true); } else { - normalPriorityMsgs.add(msg); + if (highPriority && msg.getMsgType().equals(MsgType.RULE_NODE_UPDATED_MSG)) { + synchronized (this) { + if (stopReason == TbActorStopReason.INIT_FAILED) { + destroyInProgress.set(false); + stopReason = null; + initActor(); + } else { + msg.onTbActorStopped(stopReason); + } + } + } else { + msg.onTbActorStopped(stopReason); + } } - tryProcessQueue(true); } private void tryProcessQueue(boolean newMsg) { @@ -119,6 +139,9 @@ public final class TbActorMailbox implements TbActorCtx { try { log.debug("[{}] Going to process message: {}", selfId, msg); actor.process(msg); + } catch (TbRuleNodeUpdateException updateException){ + stopReason = TbActorStopReason.INIT_FAILED; + destroy(); } catch (Throwable t) { log.debug("[{}] Failed to process message: {}", selfId, msg, t); ProcessFailureStrategy strategy = actor.onProcessFailure(t); @@ -180,11 +203,16 @@ public final class TbActorMailbox implements TbActorCtx { } public void destroy() { + if (stopReason == null) { + stopReason = TbActorStopReason.STOPPED; + } destroyInProgress.set(true); dispatcher.getExecutor().execute(() -> { try { ready.set(NOT_READY); actor.destroy(); + highPriorityMsgs.forEach(msg -> msg.onTbActorStopped(stopReason)); + normalPriorityMsgs.forEach(msg -> msg.onTbActorStopped(stopReason)); } catch (Throwable t) { log.warn("[{}] Failed to destroy actor: {}", selfId, t); } diff --git a/common/actor/src/main/java/org/thingsboard/server/actors/TbRuleNodeUpdateException.java b/common/actor/src/main/java/org/thingsboard/server/actors/TbRuleNodeUpdateException.java new file mode 100644 index 0000000000..7e3cca863d --- /dev/null +++ b/common/actor/src/main/java/org/thingsboard/server/actors/TbRuleNodeUpdateException.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2021 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; + +public class TbRuleNodeUpdateException extends RuntimeException { + + private static final long serialVersionUID = 8209771144711980882L; + + public TbRuleNodeUpdateException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java b/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java index 8ce0942f2a..31fae3d7a4 100644 --- a/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java +++ b/common/actor/src/test/java/org/thingsboard/server/actors/ActorSystemTest.java @@ -21,7 +21,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.server.common.data.id.DeviceId; import java.util.ArrayList; diff --git a/common/cache/pom.xml b/common/cache/pom.xml new file mode 100644 index 0000000000..80000b80a0 --- /dev/null +++ b/common/cache/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + + org.thingsboard + 3.3.0-SNAPSHOT + common + + org.thingsboard.common + cache + jar + + Thingsboard Server Common Cache + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + + + + + org.thingsboard.common + data + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.data + spring-data-redis + + + redis.clients + jedis + + + com.github.ben-manes.caffeine + caffeine + + + org.apache.commons + commons-lang3 + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecs.java similarity index 94% rename from dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java rename to common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecs.java index 6130942118..15a733c9ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CacheSpecs.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import lombok.Data; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java rename to common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java index f3bffb7cb9..7a0ce251fe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/CaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/CaffeineCacheConfiguration.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.RemovalCause; @@ -26,7 +26,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCache; -import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -78,14 +77,9 @@ public class CaffeineCacheConfiguration { return Ticker.systemTicker(); } - @Bean - public KeyGenerator previousDeviceCredentialsId() { - return new PreviousDeviceCredentialsIdKeyGenerator(); - } - private Weigher collectionSafeWeigher() { return (Weigher) (key, value) -> { - if(value instanceof Collection) { + if (value instanceof Collection) { return ((Collection) value).size(); } return 1; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java similarity index 95% rename from dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java rename to common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java index 5cadda82ba..b57e1579c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisCacheConfiguration.java @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.ConverterRegistry; @@ -89,11 +88,6 @@ public abstract class TBRedisCacheConfiguration { return RedisCacheManager.builder(cf).cacheDefaults(configuration).build(); } - @Bean - public KeyGenerator previousDeviceCredentialsId() { - return new PreviousDeviceCredentialsIdKeyGenerator(); - } - @Bean public RedisTemplate redisTemplate() { RedisTemplate template = new RedisTemplate<>(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java similarity index 98% rename from dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java rename to common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java index b2248989c8..912fabfbf4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java similarity index 98% rename from dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java rename to common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java index 26651f928d..4373c8b7ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisStandaloneConfiguration.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.cache; +package org.thingsboard.server.cache; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 99ec232953..ba30e5bb43 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -48,6 +48,10 @@ com.google.guava guava + + javax.annotation + javax.annotation-api + com.github.fge json-schema-validator @@ -99,7 +103,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java index d03c0d167b..57b0e2a140 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java @@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; import org.thingsboard.server.dao.cassandra.guava.GuavaSession; import org.thingsboard.server.dao.cassandra.guava.GuavaSessionBuilder; import org.thingsboard.server.dao.cassandra.guava.GuavaSessionUtils; @@ -77,7 +78,7 @@ public abstract class AbstractCassandraCluster { } private boolean isInstall() { - return environment.acceptsProfiles("install"); + return environment.acceptsProfiles(Profiles.of("install")); } private void initSession() { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java index 45f3e6fd6a..8187e5e6f2 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/guava/GuavaSessionBuilder.java @@ -18,38 +18,25 @@ package org.thingsboard.server.dao.cassandra.guava; import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.config.DriverConfigLoader; import com.datastax.oss.driver.api.core.context.DriverContext; -import com.datastax.oss.driver.api.core.metadata.Node; -import com.datastax.oss.driver.api.core.metadata.NodeStateListener; -import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.session.ProgrammaticArguments; import com.datastax.oss.driver.api.core.session.SessionBuilder; -import com.datastax.oss.driver.api.core.tracker.RequestTracker; -import com.datastax.oss.driver.api.core.type.codec.TypeCodec; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; public class GuavaSessionBuilder extends SessionBuilder { @Override protected DriverContext buildContext( DriverConfigLoader configLoader, - List> typeCodecs, - NodeStateListener nodeStateListener, - SchemaChangeListener schemaChangeListener, - RequestTracker requestTracker, - Map localDatacenters, - Map> nodeFilters, - ClassLoader classLoader) { + ProgrammaticArguments programmaticArguments) { return new GuavaDriverContext( configLoader, - typeCodecs, - nodeStateListener, - schemaChangeListener, - requestTracker, - localDatacenters, - nodeFilters, - classLoader); + programmaticArguments.getTypeCodecs(), + programmaticArguments.getNodeStateListener(), + programmaticArguments.getSchemaChangeListener(), + programmaticArguments.getRequestTracker(), + programmaticArguments.getLocalDatacenters(), + programmaticArguments.getNodeFilters(), + programmaticArguments.getClassLoader()); } @Override diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 71db29d659..f98c56d89a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; 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.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import java.util.List; @@ -45,6 +46,8 @@ public interface DeviceService { Device saveDeviceWithAccessToken(Device device, String accessToken); + Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials); + Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, CustomerId customerId); Device unassignDeviceFromCustomer(TenantId tenantId, DeviceId deviceId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java new file mode 100644 index 0000000000..d07b090494 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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.dao.resource; + +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.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; + + +public interface ResourceService { + Resource saveResource(Resource resource); + + Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + + PageData findResourcesByTenantId(TenantId tenantId, PageLink pageLink); + + void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId); + + void deleteResourcesByTenantId(TenantId tenantId); +} diff --git a/common/data/pom.xml b/common/data/pom.xml index bc977b1c52..ed544278ee 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -63,7 +63,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index 8088652588..c3490aa85f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -26,4 +26,5 @@ public class CacheConstants { public static final String SECURITY_SETTINGS_CACHE = "securitySettings"; public static final String TENANT_PROFILE_CACHE = "tenantProfiles"; public static final String DEVICE_PROFILE_CACHE = "deviceProfiles"; + public static final String ATTRIBUTES_CACHE = "attributes"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 12cc17270c..cc78e2cb45 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -24,6 +24,7 @@ public class DataConstants { public static final String CUSTOMER = "CUSTOMER"; public static final String DEVICE = "DEVICE"; + public static final String SCOPE = "scope"; public static final String CLIENT_SCOPE = "CLIENT_SCOPE"; public static final String SERVER_SCOPE = "SERVER_SCOPE"; public static final String SHARED_SCOPE = "SHARED_SCOPE"; 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 aef34d6306..ad93983cac 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 @@ -20,7 +20,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.device.data.DeviceData; -import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -64,6 +63,17 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.setDeviceData(device.getDeviceData()); } + public Device updateDevice(Device device) { + this.tenantId = device.getTenantId(); + this.customerId = device.getCustomerId(); + this.name = device.getName(); + this.type = device.getType(); + this.label = device.getLabel(); + this.deviceProfileId = device.getDeviceProfileId(); + this.setDeviceData(device.getDeviceData()); + return this; + } + public TenantId getTenantId() { return tenantId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java new file mode 100644 index 0000000000..f2985e3371 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.Data; + +@Data +public class HomeDashboard extends Dashboard { + + private boolean hideDashboardToolbar; + + public HomeDashboard(Dashboard dashboard, boolean hideDashboardToolbar) { + super(dashboard); + this.hideDashboardToolbar = hideDashboardToolbar; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java new file mode 100644 index 0000000000..1cbc1c737e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 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; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.id.DashboardId; + +@Data +@AllArgsConstructor +public class HomeDashboardInfo { + private DashboardId dashboardId; + private boolean hideDashboardToolbar; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java index 0fcb942de4..fe916f0eaa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; @@ -39,7 +39,7 @@ public class AssetSearchQuery { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(parameters); query.setFilters( - Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, Collections.singletonList(EntityType.ASSET)))); return query; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java index 2423bda7db..9143fdfece 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; @@ -36,7 +36,7 @@ public class DeviceSearchQuery { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(parameters); query.setFilters( - Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, Collections.singletonList(EntityType.DEVICE)))); return query; } 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 a13e9d05cc..bbe20bdcaa 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,7 +26,7 @@ import java.util.concurrent.TimeUnit; @JsonIgnoreProperties(ignoreUnknown = true) public class AlarmCondition { - private List condition; + private List condition; private AlarmConditionSpec spec; } 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 new file mode 100644 index 0000000000..86aafc19e5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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.device.profile; + +import lombok.Data; +import org.thingsboard.server.common.data.query.EntityKeyValueType; +import org.thingsboard.server.common.data.query.KeyFilterPredicate; + +@Data +public class AlarmConditionFilter { + + private AlarmConditionFilterKey key; + private EntityKeyValueType valueType; + private Object value; + private KeyFilterPredicate predicate; + +} 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 new file mode 100644 index 0000000000..33ee0b0628 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2021 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.device.profile; + +import lombok.Data; + +@Data +public class AlarmConditionFilterKey { + + private final AlarmConditionKeyType type; + private final String key; + +} 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 new file mode 100644 index 0000000000..d607df8e35 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionKeyType.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2021 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.device.profile; + +public enum AlarmConditionKeyType { + ATTRIBUTE, + TIME_SERIES, + ENTITY_FIELD, + CONSTANT +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java index 363832c5ce..348f7725b0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; @@ -36,7 +36,7 @@ public class EntityViewSearchQuery { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(parameters); query.setFilters( - Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, + Collections.singletonList(new RelationEntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType, Collections.singletonList(EntityType.ENTITY_VIEW)))); return query; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java new file mode 100644 index 0000000000..aeff342582 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2021 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.lwm2m; + +import lombok.Data; + +@Data +public class LwM2mInstance { + int id; + LwM2mResource [] resources; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java new file mode 100644 index 0000000000..6401e8a31a --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 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.lwm2m; + +import lombok.Data; + +@Data +public class LwM2mObject { + int id; + String name; + boolean multiple; + boolean mandatory; + LwM2mInstance [] instances; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java new file mode 100644 index 0000000000..9317232b9d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResource.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2021 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.lwm2m; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.stream.Stream; + +@Data +@AllArgsConstructor +public class LwM2mResource { + int id; + String name; + boolean observe; + boolean attribute; + boolean telemetry; + String keyName; + + public LwM2mResource(int id, String name, boolean observe, boolean attribute, boolean telemetry) { + this.id = id; + this.name = name; + this.observe = observe; + this.attribute = attribute; + this.telemetry = telemetry; + this.keyName = getCamelCase (this.name); + } + + private String getCamelCase (String name) { + name = name.replaceAll("-", " "); + name = name.replaceAll("_", " "); + String [] nameCamel1 = name.split(" "); + String [] nameCamel2 = new String[nameCamel1.length]; + int[] idx = { 0 }; + Stream.of(nameCamel1).forEach((s -> { + nameCamel2[idx[0]] = toProperCase(idx[0]++, s); + })); + return String.join("", nameCamel2); + } + + private String toProperCase(int idx, String s) { + if (!s.isEmpty() && s.length()> 0) { + String s1 = (idx == 0) ? s.substring(0, 1).toLowerCase() : s.substring(0, 1).toUpperCase(); + String s2 = ""; + if (s.length()> 1) s2 = s.substring(1).toLowerCase(); + s = s1 + s2; + } + return s; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java new file mode 100644 index 0000000000..8777e08066 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 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.lwm2m; + +import lombok.Data; + +@Data +public class ServerSecurityConfig { + String host; + Integer port; + String serverPublicKey; + boolean bootstrapServerIs = true; + Integer clientHoldOffTime = 1; + Integer serverId = 111; + Integer bootstrapServerAccountTimeout = 0; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java index 8a85158853..0e807f8c37 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AbstractDataQuery.java @@ -29,15 +29,13 @@ public abstract class AbstractDataQuery extends En protected List entityFields; @Getter protected List latestValues; - @Getter - protected List keyFilters; public AbstractDataQuery() { super(); } - public AbstractDataQuery(EntityFilter entityFilter) { - super(entityFilter); + public AbstractDataQuery(EntityFilter entityFilter, List keyFilters) { + super(entityFilter, keyFilters); } public AbstractDataQuery(EntityFilter entityFilter, @@ -45,11 +43,10 @@ public abstract class AbstractDataQuery extends En List entityFields, List latestValues, List keyFilters) { - super(entityFilter); + super(entityFilter, keyFilters); this.pageLink = pageLink; this.entityFields = entityFields; this.latestValues = latestValues; - this.keyFilters = keyFilters; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java index 590073410c..655ef7ecfd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataQuery.java @@ -30,8 +30,8 @@ public class AlarmDataQuery extends AbstractDataQuery { public AlarmDataQuery() { } - public AlarmDataQuery(EntityFilter entityFilter) { - super(entityFilter); + public AlarmDataQuery(EntityFilter entityFilter, List keyFilters) { + super(entityFilter, keyFilters); } public AlarmDataQuery(EntityFilter entityFilter, AlarmDataPageLink pageLink, List entityFields, List latestValues, List keyFilters, List alarmFields) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java index 3d46f8d2ab..2fa0a4d36a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/DynamicValue.java @@ -17,17 +17,23 @@ package org.thingsboard.server.common.data.query; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import lombok.Getter; +import lombok.RequiredArgsConstructor; @Data +@RequiredArgsConstructor public class DynamicValue { @JsonIgnore private T resolvedValue; - @Getter private final DynamicValueSourceType sourceType; - @Getter private final String sourceAttribute; + private final boolean inherit; + + public DynamicValue(DynamicValueSourceType sourceType, String sourceAttribute) { + this.sourceAttribute = sourceAttribute; + this.sourceType = sourceType; + this.inherit = false; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java index b056b31dac..07878a3347 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -17,14 +17,26 @@ package org.thingsboard.server.common.data.query; import lombok.Getter; +import java.util.Collections; +import java.util.List; + public class EntityCountQuery { @Getter private EntityFilter entityFilter; - public EntityCountQuery() {} + @Getter + protected List keyFilters; + + public EntityCountQuery() { + } public EntityCountQuery(EntityFilter entityFilter) { + this(entityFilter, Collections.emptyList()); + } + + public EntityCountQuery(EntityFilter entityFilter, List keyFilters) { this.entityFilter = entityFilter; + this.keyFilters = keyFilters; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java index a7342d13f3..aca8437fe8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityDataQuery.java @@ -27,8 +27,8 @@ public class EntityDataQuery extends AbstractDataQuery { public EntityDataQuery() { } - public EntityDataQuery(EntityFilter entityFilter) { - super(entityFilter); + public EntityDataQuery(EntityFilter entityFilter, List keyFilters) { + super(entityFilter, keyFilters); } public EntityDataQuery(EntityFilter entityFilter, EntityDataPageLink pageLink, List entityFields, List latestValues, List keyFilters) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index 78ef869f56..efdd70ec7b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = SingleEntityFilter.class, name = "singleEntity"), @JsonSubTypes.Type(value = EntityListFilter.class, name = "entityList"), @JsonSubTypes.Type(value = EntityNameFilter.class, name = "entityName"), + @JsonSubTypes.Type(value = EntityTypeFilter.class, name = "entityType"), @JsonSubTypes.Type(value = AssetTypeFilter.class, name = "assetType"), @JsonSubTypes.Type(value = DeviceTypeFilter.class, name = "deviceType"), @JsonSubTypes.Type(value = EntityViewTypeFilter.class, name = "entityViewType"), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java index 62c7546e8e..6b590c4695 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java @@ -19,6 +19,7 @@ public enum EntityFilterType { SINGLE_ENTITY("singleEntity"), ENTITY_LIST("entityList"), ENTITY_NAME("entityName"), + ENTITY_TYPE("entityType"), ASSET_TYPE("assetType"), DEVICE_TYPE("deviceType"), ENTITY_VIEW_TYPE("entityViewType"), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java new file mode 100644 index 0000000000..22c2212a84 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityTypeFilter.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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.query; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; + +@Data +public class EntityTypeFilter implements EntityFilter { + @Override + public EntityFilterType getType() { + return EntityFilterType.ENTITY_TYPE; + } + + private EntityType entityType; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java index 0890f48c3a..9c113eb793 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java @@ -18,8 +18,7 @@ package org.thingsboard.server.common.data.query; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; @@ -33,7 +32,7 @@ public class RelationsQueryFilter implements EntityFilter { private EntityId rootEntity; private EntitySearchDirection direction; - private List filters; + private List filters; private int maxLevel; private boolean fetchLastLevelOnly; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java index b673f60462..1a5415d3c7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java @@ -26,6 +26,6 @@ import java.util.List; public class EntityRelationsQuery { private RelationsSearchParameters parameters; - private List filters; + private List filters; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java similarity index 95% rename from common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityTypeFilter.java rename to common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java index 8b9849d6a1..1e817dda14 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java @@ -26,7 +26,7 @@ import java.util.List; */ @Data @AllArgsConstructor -public class EntityTypeFilter { +public class RelationEntityTypeFilter { private String relationType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java index dddf0e2810..9df283f425 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java @@ -19,6 +19,6 @@ public enum DeviceCredentialsType { ACCESS_TOKEN, X509_CERTIFICATE, - MQTT_BASIC - + MQTT_BASIC, + LWM2M_CREDENTIALS } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/Resource.java similarity index 53% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java rename to common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/Resource.java index 69334f2e8f..c29b704b04 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/BasicCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/Resource.java @@ -13,30 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.mqtt.credentials; +package org.thingsboard.server.common.data.transport.resource; -import io.netty.handler.ssl.SslContext; import lombok.Data; -import org.thingsboard.mqtt.MqttClientConfig; - -import java.util.Optional; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.TenantId; @Data -public class BasicCredentials implements MqttClientCredentials { - - private String username; - private String password; +public class Resource implements HasTenantId { + private TenantId tenantId; + private ResourceType resourceType; + private String resourceId; + private String value; @Override - public Optional initSslContext() { - return Optional.empty(); + public String toString() { + return "Resource{" + + "tenantId=" + tenantId + + ", resourceType=" + resourceType + + ", resourceId='" + resourceId + '\'' + + '}'; } - - @Override - public void configure(MqttClientConfig config) { - config.setUsername(username); - config.setPassword(password); - } - } - diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java new file mode 100644 index 0000000000..c43f1997da --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/resource/ResourceType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2021 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.transport.resource; + +public enum ResourceType { + LWM2M_MODEL, JKS, PKCS_12 +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java index adb173f367..0f5bf5f724 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java @@ -19,7 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; import java.util.Arrays; diff --git a/common/message/pom.xml b/common/message/pom.xml index a9366c9de2..06ba4aff3b 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -76,7 +76,7 @@ org.mockito - mockito-all + mockito-core test 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 495d5bf5ea..13d0a6e9eb 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 @@ -39,6 +39,11 @@ public enum MsgType { */ COMPONENT_LIFE_CYCLE_MSG, + /** + * Special message to indicate rule node update request + */ + RULE_NODE_UPDATED_MSG, + /** * Misc messages consumed from the Queue and forwarded to Rule Engine Actor. * @@ -66,11 +71,6 @@ public enum MsgType { */ REMOTE_TO_RULE_CHAIN_TELL_NEXT_MSG, - /** - * Message that is sent by RuleActor implementation to RuleActor itself to log the error. - */ - RULE_TO_SELF_ERROR_MSG, - /** * Message that is sent by RuleActor implementation to RuleActor itself to process the message. */ diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorMsg.java index fe35d47d88..b7ba251704 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorMsg.java @@ -22,4 +22,12 @@ public interface TbActorMsg { MsgType getMsgType(); + /** + * Executed when the target TbActor is stopped or destroyed. + * For example, rule node failed to initialize or removed from rule chain. + * Implementation should cleanup the resources. + */ + default void onTbActorStopped(TbActorStopReason reason) { + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorStopReason.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorStopReason.java new file mode 100644 index 0000000000..32035a7352 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbActorStopReason.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2021 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; + +public enum TbActorStopReason { + + INIT_FAILED, STOPPED + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 348e8021e4..66a3670ddd 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -120,7 +120,7 @@ public final class TbMsg implements Serializable { private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId, int ruleNodeExecCounter, TbMsgCallback callback) { this.id = id; - this.queueName = queueName; + this.queueName = queueName != null ? queueName : ServiceQueue.MAIN; if (ts > 0) { this.ts = ts; } else { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbRuleEngineActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbRuleEngineActorMsg.java new file mode 100644 index 0000000000..64bb772b4f --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbRuleEngineActorMsg.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +public abstract class TbRuleEngineActorMsg implements TbActorMsg { + + @Getter + protected final TbMsg msg; + + public TbRuleEngineActorMsg(TbMsg msg) { + this.msg = msg; + } +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/RuleNodeUpdatedMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/RuleNodeUpdatedMsg.java new file mode 100644 index 0000000000..ca4ab1c949 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/RuleNodeUpdatedMsg.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2021 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.plugin; + +import lombok.ToString; +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.msg.MsgType; + +import java.util.Optional; + +/** + * @author Andrew Shvayka + */ +@ToString +public class RuleNodeUpdatedMsg extends ComponentLifecycleMsg { + + public RuleNodeUpdatedMsg(TenantId tenantId, EntityId entityId) { + super(tenantId, entityId, ComponentLifecycleEvent.UPDATED); + } + + @Override + public MsgType getMsgType() { + return MsgType.RULE_NODE_UPDATED_MSG; + } +} \ No newline at end of file diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/QueueToRuleEngineMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/QueueToRuleEngineMsg.java index ded2fa722c..7648fdab6d 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/QueueToRuleEngineMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/QueueToRuleEngineMsg.java @@ -15,32 +15,58 @@ */ package org.thingsboard.server.common.msg.queue; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.TbActorMsg; +import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbRuleEngineActorMsg; -import java.io.Serializable; import java.util.Set; /** * Created by ashvayka on 15.03.18. */ -@Data -public final class QueueToRuleEngineMsg implements TbActorMsg { +@ToString +@EqualsAndHashCode(callSuper = true) +public final class QueueToRuleEngineMsg extends TbRuleEngineActorMsg { + @Getter private final TenantId tenantId; - private final TbMsg tbMsg; + @Getter private final Set relationTypes; + @Getter private final String failureMessage; + public QueueToRuleEngineMsg(TenantId tenantId, TbMsg tbMsg, Set relationTypes, String failureMessage) { + super(tbMsg); + this.tenantId = tenantId; + this.relationTypes = relationTypes; + this.failureMessage = failureMessage; + } + @Override public MsgType getMsgType() { return MsgType.QUEUE_TO_RULE_ENGINE_MSG; } + @Override + public void onTbActorStopped(TbActorStopReason reason) { + String message; + if (msg.getRuleChainId() != null) { + message = reason == TbActorStopReason.STOPPED ? + String.format("Rule chain [%s] stopped", msg.getRuleChainId().getId()) : + String.format("Failed to initialize rule chain [%s]!", msg.getRuleChainId().getId()); + } else { + message = reason == TbActorStopReason.STOPPED ? "Rule chain stopped" : "Failed to initialize rule chain!"; + } + msg.getCallback().onFailure(new RuleEngineException(message)); + } + public boolean isTellNext() { return relationTypes != null && !relationTypes.isEmpty(); } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java index 8e68d1f94c..cd073f3a6e 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java @@ -24,6 +24,9 @@ import org.thingsboard.server.common.data.rule.RuleNode; @Slf4j public class RuleNodeException extends RuleEngineException { + + private static final long serialVersionUID = -1776681087370749776L; + @Getter private final String ruleChainName; @Getter @@ -33,6 +36,7 @@ public class RuleNodeException extends RuleEngineException { @Getter private final RuleNodeId ruleNodeId; + public RuleNodeException(String message, String ruleChainName, RuleNode ruleNode) { super(message); this.ruleChainName = ruleChainName; diff --git a/common/pom.xml b/common/pom.xml index 186c5ecdd8..7568d18ca6 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard common @@ -42,6 +42,7 @@ transport dao-api stats + cache diff --git a/common/queue/pom.xml b/common/queue/pom.xml index e4d9e1bf20..e0b30dd654 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -124,7 +124,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusConsumerTemplate.java index 5b562cbfbf..525f9bfe92 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusConsumerTemplate.java @@ -154,6 +154,7 @@ public class TbServiceBusConsumerTemplate extends Abstract } private CompletableFuture> fromList(List> futures) { + @SuppressWarnings("unchecked") CompletableFuture>[] arrayFuture = new CompletableFuture[futures.size()]; futures.toArray(arrayFuture); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ClusterTopologyChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ClusterTopologyChangeEvent.java index 0602dee3ae..1e5b90b5fe 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ClusterTopologyChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/ClusterTopologyChangeEvent.java @@ -22,7 +22,9 @@ import org.thingsboard.server.common.msg.queue.ServiceQueueKey; import java.util.Set; -public class ClusterTopologyChangeEvent extends ApplicationEvent { +public class ClusterTopologyChangeEvent extends TbApplicationEvent { + + private static final long serialVersionUID = -2441739930040282254L; @Getter private final Set serviceQueueKeys; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index d8164f4be8..2da438417a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -126,7 +126,7 @@ public class HashPartitionService implements PartitionService { } @Override - public void recalculatePartitions(ServiceInfo currentService, List otherServices) { + public synchronized void recalculatePartitions(ServiceInfo currentService, List otherServices) { logServiceInfo(currentService); otherServices.forEach(this::logServiceInfo); Map> queueServicesMap = new HashMap<>(); @@ -134,7 +134,7 @@ public class HashPartitionService implements PartitionService { for (ServiceInfo other : otherServices) { addNode(queueServicesMap, other); } - queueServicesMap.values().forEach(list -> list.sort((a, b) -> a.getServiceId().compareTo(b.getServiceId()))); + queueServicesMap.values().forEach(list -> list.sort(Comparator.comparing(ServiceInfo::getServiceId))); ConcurrentMap> oldPartitions = myPartitions; TenantId myIsolatedOrSystemTenantId = getSystemOrIsolatedTenantId(currentService); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionChangeEvent.java index e4edabbe19..2edcd2ceca 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/PartitionChangeEvent.java @@ -24,7 +24,9 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import java.util.Set; -public class PartitionChangeEvent extends ApplicationEvent { +public class PartitionChangeEvent extends TbApplicationEvent { + + private static final long serialVersionUID = -8731788167026510559L; @Getter private final ServiceQueueKey serviceQueueKey; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEvent.java new file mode 100644 index 0000000000..face2d36d6 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEvent.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2021 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.queue.discovery; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +import java.util.concurrent.atomic.AtomicInteger; + +public class TbApplicationEvent extends ApplicationEvent { + + private static final long serialVersionUID = 3884264064887765146L; + + private static final AtomicInteger sequence = new AtomicInteger(); + + @Getter + private final int sequenceNumber; + + public TbApplicationEvent(Object source) { + super(source); + sequenceNumber = sequence.incrementAndGet(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java new file mode 100644 index 0000000000..9158d8f0c8 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbApplicationEventListener.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 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.queue.discovery; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationListener; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +@Slf4j +public abstract class TbApplicationEventListener implements ApplicationListener { + + private int lastProcessedSequenceNumber = Integer.MIN_VALUE; + private final Lock seqNumberLock = new ReentrantLock(); + + @Override + public void onApplicationEvent(T event) { + boolean validUpdate = false; + seqNumberLock.lock(); + try { + if (event.getSequenceNumber() > lastProcessedSequenceNumber) { + validUpdate = true; + lastProcessedSequenceNumber = event.getSequenceNumber(); + } + } finally { + seqNumberLock.unlock(); + } + if (validUpdate) { + onTbApplicationEvent(event); + } else { + log.info("Application event ignored due to invalid sequence number ({} > {}). Event: {}", lastProcessedSequenceNumber, event.getSequenceNumber(), event); + } + } + + protected abstract void onTbApplicationEvent(T event); + + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatisticConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatisticConfig.java new file mode 100644 index 0000000000..bfe7d29bb6 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatisticConfig.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2021 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.queue.kafka; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class TbKafkaConsumerStatisticConfig { + @Value("${queue.kafka.consumer-stats.enabled:true}") + private Boolean enabled; + @Value("${queue.kafka.consumer-stats.print-interval-ms:60000}") + private Long printIntervalMs; + @Value("${queue.kafka.consumer-stats.kafka-response-timeout-ms:1000}") + private Long kafkaResponseTimeoutMs; +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java new file mode 100644 index 0000000000..0ccf9ba42c --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java @@ -0,0 +1,184 @@ +/** + * Copyright © 2016-2021 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.queue.kafka; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.queue.discovery.PartitionService; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "queue", value = "type", havingValue = "kafka") +public class TbKafkaConsumerStatsService { + private final Set monitoredGroups = ConcurrentHashMap.newKeySet(); + + private final TbKafkaSettings kafkaSettings; + private final TbKafkaConsumerStatisticConfig statsConfig; + private final PartitionService partitionService; + + private AdminClient adminClient; + private Consumer consumer; + private ScheduledExecutorService statsPrintScheduler; + + @PostConstruct + public void init() { + if (!statsConfig.getEnabled()) { + return; + } + this.adminClient = AdminClient.create(kafkaSettings.toAdminProps()); + this.statsPrintScheduler = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("kafka-consumer-stats")); + + Properties consumerProps = kafkaSettings.toConsumerProps(); + consumerProps.put(ConsumerConfig.CLIENT_ID_CONFIG, "consumer-stats-loader-client"); + consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer-stats-loader-client-group"); + this.consumer = new KafkaConsumer<>(consumerProps); + + startLogScheduling(); + } + + private void startLogScheduling() { + Duration timeoutDuration = Duration.ofMillis(statsConfig.getKafkaResponseTimeoutMs()); + statsPrintScheduler.scheduleWithFixedDelay(() -> { + if (!isStatsPrintRequired()) { + return; + } + for (String groupId : monitoredGroups) { + try { + Map groupOffsets = adminClient.listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata() + .get(statsConfig.getKafkaResponseTimeoutMs(), TimeUnit.MILLISECONDS); + Map endOffsets = consumer.endOffsets(groupOffsets.keySet(), timeoutDuration); + + List lagTopicsStats = getTopicsStatsWithLag(groupOffsets, endOffsets); + if (!lagTopicsStats.isEmpty()) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < lagTopicsStats.size(); i++) { + builder.append(lagTopicsStats.get(i).toString()); + if (i != lagTopicsStats.size() - 1) { + builder.append(", "); + } + } + log.info("[{}] Topic partitions with lag: [{}].", groupId, builder.toString()); + } + } catch (Exception e) { + log.warn("[{}] Failed to get consumer group stats. Reason - {}.", groupId, e.getMessage()); + log.trace("Detailed error: ", e); + } + } + + }, statsConfig.getPrintIntervalMs(), statsConfig.getPrintIntervalMs(), TimeUnit.MILLISECONDS); + } + + private boolean isStatsPrintRequired() { + boolean isMyRuleEnginePartition = partitionService.resolve(ServiceType.TB_RULE_ENGINE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); + boolean isMyCorePartition = partitionService.resolve(ServiceType.TB_CORE, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isMyPartition(); + return log.isInfoEnabled() && (isMyRuleEnginePartition || isMyCorePartition); + } + + private List getTopicsStatsWithLag(Map groupOffsets, Map endOffsets) { + List consumerGroupStats = new ArrayList<>(); + for (TopicPartition topicPartition : groupOffsets.keySet()) { + long endOffset = endOffsets.get(topicPartition); + long committedOffset = groupOffsets.get(topicPartition).offset(); + long lag = endOffset - committedOffset; + if (lag != 0) { + GroupTopicStats groupTopicStats = GroupTopicStats.builder() + .topic(topicPartition.topic()) + .partition(topicPartition.partition()) + .committedOffset(committedOffset) + .endOffset(endOffset) + .lag(lag) + .build(); + consumerGroupStats.add(groupTopicStats); + } + } + return consumerGroupStats; + } + + public void registerClientGroup(String groupId) { + if (statsConfig.getEnabled() && !StringUtils.isEmpty(groupId)) { + monitoredGroups.add(groupId); + } + } + + public void unregisterClientGroup(String groupId) { + if (statsConfig.getEnabled() && !StringUtils.isEmpty(groupId)) { + monitoredGroups.remove(groupId); + } + } + + @PreDestroy + public void destroy() { + if (statsPrintScheduler != null) { + statsPrintScheduler.shutdownNow(); + } + if (adminClient != null) { + adminClient.close(); + } + if (consumer != null) { + consumer.close(); + } + } + + + @Builder + @Data + private static class GroupTopicStats { + private String topic; + private int partition; + private long committedOffset; + private long endOffset; + private long lag; + + @Override + public String toString() { + return "[" + + "topic=[" + topic + ']' + + ", partition=[" + partition + "]" + + ", committedOffset=[" + committedOffset + "]" + + ", endOffset=[" + endOffset + "]" + + ", lag=[" + lag + "]" + + "]"; + } + } +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index 8743f4d6cc..b1b3d5ce05 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -42,10 +42,13 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue private final KafkaConsumer consumer; private final TbKafkaDecoder decoder; + private final TbKafkaConsumerStatsService statsService; + private final String groupId; + @Builder private TbKafkaConsumerTemplate(TbKafkaSettings settings, TbKafkaDecoder decoder, String clientId, String groupId, String topic, - TbQueueAdmin admin) { + TbQueueAdmin admin, TbKafkaConsumerStatsService statsService) { super(topic); Properties props = settings.toConsumerProps(); props.put(ConsumerConfig.CLIENT_ID_CONFIG, clientId); @@ -53,6 +56,13 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); } + this.statsService = statsService; + this.groupId = groupId; + + if (statsService != null) { + statsService.registerClientGroup(groupId); + } + this.admin = admin; this.consumer = new KafkaConsumer<>(props); this.decoder = decoder; @@ -96,6 +106,8 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue consumer.unsubscribe(); consumer.close(); } + if (statsService != null) { + statsService.unregisterClientGroup(groupId); + } } - } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 4c35c0ca1f..86738acea8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -22,6 +22,10 @@ import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.ByteArrayDeserializer; +import org.apache.kafka.common.serialization.ByteArraySerializer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -107,8 +111,8 @@ public class TbKafkaSettings { props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes); props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); return props; } @@ -120,8 +124,8 @@ public class TbKafkaSettings { props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize); props.put(ProducerConfig.LINGER_MS_CONFIG, lingerMs); props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory); - props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); - props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); return props; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java index b46fd4c87d..226ed32682 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryStorage.java @@ -60,6 +60,7 @@ public final class InMemoryStorage { public List get(String topic) throws InterruptedException { if (storage.containsKey(topic)) { List entities; + @SuppressWarnings("unchecked") T first = (T) storage.get(topic).poll(); if (first != null) { entities = new ArrayList<>(); @@ -67,7 +68,9 @@ public final class InMemoryStorage { List otherList = new ArrayList<>(); storage.get(topic).drainTo(otherList, 999); for (TbQueueMsg other : otherList) { - entities.add((T) other); + @SuppressWarnings("unchecked") + T entity = (T) other; + entities.add(entity); } } else { entities = Collections.emptyList(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java index 95af725bb4..6e28d48a4b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/memory/InMemoryTbQueueConsumer.java @@ -64,6 +64,7 @@ public class InMemoryTbQueueConsumer implements TbQueueCon @Override public List poll(long durationInMillis) { if (subscribed) { + @SuppressWarnings("unchecked") List messages = partitions .stream() .map(tpi -> { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index eebffc37b0..e07f3e43d3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -39,6 +39,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; @@ -65,6 +66,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -79,6 +81,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { this.partitionService = partitionService; this.kafkaSettings = kafkaSettings; @@ -88,6 +91,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; this.jsInvokeSettings = jsInvokeSettings; + this.consumerStatsService = consumerStatsService; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -156,6 +160,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi consumerBuilder.groupId("re-" + queueName + "-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(ruleEngineAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -168,6 +173,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi consumerBuilder.groupId("monolith-rule-engine-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -180,6 +186,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi consumerBuilder.groupId("monolith-core-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -192,6 +199,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi consumerBuilder.groupId("monolith-core-notifications-consumer-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -204,6 +212,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi consumerBuilder.groupId("monolith-transport-api-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(transportApiAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -237,6 +246,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi return new TbProtoQueueMsg<>(msg.getKey(), builder.build(), msg.getHeaders()); } ); + responseBuilder.statsService(consumerStatsService); responseBuilder.admin(jsExecutorAdmin); DefaultTbQueueRequestTemplate.DefaultTbQueueRequestTemplateBuilder @@ -259,6 +269,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi consumerBuilder.groupId("monolith-us-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index 15771a3b78..24e80adec5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -39,6 +39,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; @@ -62,6 +63,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -75,6 +77,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbQueueRuleEngineSettings ruleEngineSettings, TbQueueTransportApiSettings transportApiSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { this.partitionService = partitionService; this.kafkaSettings = kafkaSettings; @@ -83,6 +86,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { this.ruleEngineSettings = ruleEngineSettings; this.transportApiSettings = transportApiSettings; this.jsInvokeSettings = jsInvokeSettings; + this.consumerStatsService = consumerStatsService; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -150,6 +154,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { consumerBuilder.groupId("tb-core-node"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -162,6 +167,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { consumerBuilder.groupId("tb-core-notifications-node-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -174,6 +180,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { consumerBuilder.groupId("tb-core-transport-api-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(transportApiAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -208,6 +215,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { } ); responseBuilder.admin(jsExecutorAdmin); + responseBuilder.statsService(consumerStatsService); DefaultTbQueueRequestTemplate.DefaultTbQueueRequestTemplateBuilder , TbProtoQueueMsg> builder = DefaultTbQueueRequestTemplate.builder(); @@ -229,6 +237,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { consumerBuilder.groupId("tb-core-us-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToUsageStatsServiceMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(coreAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index b45ec62b04..a8247dc11e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -37,6 +37,7 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; @@ -59,6 +60,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { private final TbQueueCoreSettings coreSettings; private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueRemoteJsInvokeSettings jsInvokeSettings; + private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -70,6 +72,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { TbQueueCoreSettings coreSettings, TbQueueRuleEngineSettings ruleEngineSettings, TbQueueRemoteJsInvokeSettings jsInvokeSettings, + TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { this.partitionService = partitionService; this.kafkaSettings = kafkaSettings; @@ -77,6 +80,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { this.coreSettings = coreSettings; this.ruleEngineSettings = ruleEngineSettings; this.jsInvokeSettings = jsInvokeSettings; + this.consumerStatsService = consumerStatsService; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -145,6 +149,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { consumerBuilder.groupId("re-" + queueName + "-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(ruleEngineAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -157,6 +162,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { consumerBuilder.groupId("tb-rule-engine-notifications-node-" + serviceInfoProvider.getServiceId()); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(notificationAdmin); + consumerBuilder.statsService(consumerStatsService); return consumerBuilder.build(); } @@ -181,6 +187,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { } ); responseBuilder.admin(jsExecutorAdmin); + responseBuilder.statsService(consumerStatsService); DefaultTbQueueRequestTemplate.DefaultTbQueueRequestTemplateBuilder , TbProtoQueueMsg> builder = DefaultTbQueueRequestTemplate.builder(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java index 0c64a57304..8c0c6999d5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbTransportQueueFactory.java @@ -32,6 +32,7 @@ import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; +import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; import org.thingsboard.server.queue.kafka.TbKafkaSettings; @@ -54,6 +55,7 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { private final TbQueueRuleEngineSettings ruleEngineSettings; private final TbQueueTransportApiSettings transportApiSettings; private final TbQueueTransportNotificationSettings transportNotificationSettings; + private final TbKafkaConsumerStatsService consumerStatsService; private final TbQueueAdmin coreAdmin; private final TbQueueAdmin ruleEngineAdmin; @@ -66,6 +68,7 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { TbQueueRuleEngineSettings ruleEngineSettings, TbQueueTransportApiSettings transportApiSettings, TbQueueTransportNotificationSettings transportNotificationSettings, + TbKafkaConsumerStatsService consumerStatsService, TbKafkaTopicConfigs kafkaTopicConfigs) { this.kafkaSettings = kafkaSettings; this.serviceInfoProvider = serviceInfoProvider; @@ -73,6 +76,7 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { this.ruleEngineSettings = ruleEngineSettings; this.transportApiSettings = transportApiSettings; this.transportNotificationSettings = transportNotificationSettings; + this.consumerStatsService = consumerStatsService; this.coreAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getCoreConfigs()); this.ruleEngineAdmin = new TbKafkaAdmin(kafkaSettings, kafkaTopicConfigs.getRuleEngineConfigs()); @@ -95,6 +99,7 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { responseBuilder.groupId("transport-node-" + serviceInfoProvider.getServiceId()); responseBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiResponseMsg.parseFrom(msg.getData()), msg.getHeaders())); responseBuilder.admin(transportApiAdmin); + responseBuilder.statsService(consumerStatsService); DefaultTbQueueRequestTemplate.DefaultTbQueueRequestTemplateBuilder , TbProtoQueueMsg> templateBuilder = DefaultTbQueueRequestTemplate.builder(); @@ -136,6 +141,7 @@ public class KafkaTbTransportQueueFactory implements TbTransportQueueFactory { responseBuilder.groupId("transport-node-" + serviceInfoProvider.getServiceId()); responseBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToTransportMsg.parseFrom(msg.getData()), msg.getHeaders())); responseBuilder.admin(notificationAdmin); + responseBuilder.statsService(consumerStatsService); return responseBuilder.build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java index 066475d5ba..01a322f158 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java @@ -47,6 +47,7 @@ public class DefaultTbApiUsageClient implements TbApiUsageClient { @Value("${usage.stats.report.interval:10}") private int interval; + @SuppressWarnings("unchecked") private final ConcurrentMap[] values = new ConcurrentMap[ApiUsageRecordKey.values().length]; private final PartitionService partitionService; private final SchedulerComponent scheduler; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mTransportComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mTransportComponent.java new file mode 100644 index 0000000000..638f9f6fb2 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/TbLwM2mTransportComponent.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2021 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.queue.util; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled}'=='true')") +public @interface TbLwM2mTransportComponent { +} diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 6864259617..42f6267d42 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -77,6 +77,7 @@ enum CredentialsType { ACCESS_TOKEN = 0; X509_CERTIFICATE = 1; MQTT_BASIC = 2; + LWM2M_CREDENTIALS = 3; } message KeyValueProto { @@ -183,6 +184,66 @@ message GetEntityProfileRequestMsg { int64 entityIdLSB = 3; } +message LwM2MRegistrationRequestMsg { + string tenantId = 1; + string endpoint = 2; +} + +message LwM2MRegistrationResponseMsg { + DeviceInfoProto deviceInfo = 1; +} + +message LwM2MRequestMsg { + LwM2MRegistrationRequestMsg registrationMsg = 1; +} + +message LwM2MResponseMsg { + LwM2MRegistrationResponseMsg registrationMsg = 1; +} + +message ResourceMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string resourceType = 3; + string resourceId = 4; + string value = 5; +} + +message GetResourcesRequestMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string resourceType = 3; + string resourceId = 4; +} + +message GetResourcesResponseMsg { + repeated ResourceMsg resources = 1; +} + +message ValidateDeviceLwM2MCredentialsRequestMsg { + string credentialsId = 1; +} + +message ToTransportUpdateCredentialsProto { + repeated string credentialsId = 1; + repeated string credentialsValue = 2; +} + +message GetTenantRoutingInfoRequestMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; +} + +message GetTenantRoutingInfoResponseMsg { + bool isolatedTbCore = 1; + bool isolatedTbRuleEngine = 2; +} + +message GetDeviceProfileRequestMsg { + int64 profileIdMSB = 1; + int64 profileIdLSB = 2; +} + message GetEntityProfileResponseMsg { string entityType = 1; bytes data = 2; @@ -200,6 +261,20 @@ message EntityDeleteMsg { int64 entityIdLSB = 3; } +message ResourceUpdateMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string resourceType = 3; + string resourceId = 4; +} + +message ResourceDeleteMsg { + int64 tenantIdMSB = 1; + int64 tenantIdLSB = 2; + string resourceType = 3; + string resourceId = 4; +} + message SessionCloseNotificationProto { string message = 1; } @@ -479,8 +554,11 @@ message TransportApiRequestMsg { ValidateDeviceX509CertRequestMsg validateX509CertRequestMsg = 2; GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3; GetEntityProfileRequestMsg entityProfileRequestMsg = 4; + LwM2MRequestMsg lwM2MRequestMsg = 5; ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; ProvisionDeviceRequestMsg provisionDeviceRequestMsg = 7; + ValidateDeviceLwM2MCredentialsRequestMsg validateDeviceLwM2MCredentialsRequestMsg = 8; + GetResourcesRequestMsg resourcesRequestMsg = 9; } /* Response from ThingsBoard Core Service to Transport Service */ @@ -489,6 +567,8 @@ message TransportApiResponseMsg { GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; GetEntityProfileResponseMsg entityProfileResponseMsg = 3; ProvisionDeviceResponseMsg provisionDeviceResponseMsg = 4; + LwM2MResponseMsg lwM2MResponseMsg = 6; + GetResourcesResponseMsg resourcesResponseMsg = 7; } /* Messages that are handled by ThingsBoard Core Service */ @@ -529,10 +609,12 @@ message ToTransportMsg { AttributeUpdateNotificationMsg attributeUpdateNotification = 5; ToDeviceRpcRequestMsg toDeviceRequest = 6; ToServerRpcResponseMsg toServerResponse = 7; - /* For Tenant, TenantProfile and DeviceProfile */ EntityUpdateMsg entityUpdateMsg = 8; EntityDeleteMsg entityDeleteMsg = 9; ProvisionDeviceResponseMsg provisionResponse = 10; + ToTransportUpdateCredentialsProto toTransportUpdateCredentialsNotification = 11; + ResourceUpdateMsg resourceUpdateMsg = 12; + ResourceDeleteMsg resourceDeleteMsg = 13; } message UsageStatsKVProto{ diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 830c5bb3b5..005c210b7b 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -79,7 +79,7 @@ org.mockito - mockito-all + mockito-core test @@ -89,4 +89,4 @@ - \ No newline at end of file + diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 2d7c0df235..cea8ddc0ce 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.common.transport @@ -80,7 +80,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java index 78b79e0b8e..589ae3d41e 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportContext.java @@ -24,6 +24,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; + /** * Created by ashvayka on 18.10.18. */ diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 7ba4667dfb..9332502207 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -16,14 +16,16 @@ package org.thingsboard.server.transport.coap; import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapObserveRelation; import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.CoAP.ResponseCode; import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.network.Endpoint; import org.eclipse.californium.core.network.Exchange; -import org.eclipse.californium.core.network.ExchangeObserver; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; -import org.springframework.util.ReflectionUtils; +import org.eclipse.californium.elements.EndpointContext; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.security.DeviceTokenCredentials; @@ -40,26 +42,26 @@ import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsRes import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; -import java.lang.reflect.Field; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @Slf4j public class CoapTransportResource extends CoapResource { - // coap://localhost:port/api/v1/DEVICE_TOKEN/[attributes|telemetry|rpc[/requestId]] private static final int ACCESS_TOKEN_POSITION = 3; private static final int FEATURE_TYPE_POSITION = 4; private static final int REQUEST_ID_POSITION = 5; private final CoapTransportContext transportContext; private final TransportService transportService; - private final Field observerField; private final long timeout; private final ConcurrentMap tokenToSessionIdMap = new ConcurrentHashMap<>(); private final Set rpcSubscriptions = ConcurrentHashMap.newKeySet(); @@ -73,9 +75,20 @@ public class CoapTransportResource extends CoapResource { // This is important to turn off existing observable logic in // CoapResource. We will have our own observe monitoring due to 1:1 // observe relationship. - this.setObservable(false); - observerField = ReflectionUtils.findField(Exchange.class, "observer"); - observerField.setAccessible(true); + this.setObservable(true); // enable observing + this.setObserveType(CoAP.Type.CON); // configure the notification type to CONs + this.getAttributes().setObservable(); // mark observable in the Link-Format + // schedule a periodic update task, otherwise let events call changed() + Timer timer = new Timer(); + timer.schedule(new UpdateTask(), 0, 5000); + } + + private class UpdateTask extends TimerTask { + @Override + public void run() { + // .. periodic update of the resource + changed(); // notify all observers + } } @Override @@ -187,9 +200,7 @@ public class CoapTransportResource extends CoapResource { new CoapOkCallback(exchange)); break; case SUBSCRIBE_ATTRIBUTES_REQUEST: - attributeSubscriptions.add(sessionId); - advanced.setObserver(new CoapExchangeObserverProxy((ExchangeObserver) observerField.get(advanced), - registerAsyncCoapSession(exchange, request, sessionInfo, sessionId))); + transportService.registerSyncSession(sessionInfo, new CoapSessionListener(sessionId, exchange), transportContext.getTimeout()); transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); @@ -206,8 +217,6 @@ public class CoapTransportResource extends CoapResource { break; case SUBSCRIBE_RPC_COMMANDS_REQUEST: rpcSubscriptions.add(sessionId); - advanced.setObserver(new CoapExchangeObserverProxy((ExchangeObserver) observerField.get(advanced), - registerAsyncCoapSession(exchange, request, sessionInfo, sessionId))); transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.getDefaultInstance(), new CoapNoOpCallback(exchange)); @@ -243,9 +252,6 @@ public class CoapTransportResource extends CoapResource { } catch (AdaptorException e) { log.trace("[{}] Failed to decode message: ", sessionId, e); exchange.respond(ResponseCode.BAD_REQUEST); - } catch (IllegalAccessException e) { - log.trace("[{}] Failed to process message: ", sessionId, e); - exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR); } })); } @@ -259,12 +265,15 @@ public class CoapTransportResource extends CoapResource { } private TransportProtos.SessionInfoProto lookupAsyncSessionInfo(Request request) { - String token = request.getSource().getHostAddress() + ":" + request.getSourcePort() + ":" + request.getTokenString(); + EndpointContext sourceContext = request.getSourceContext(); + String token = sourceContext.getPeerAddress().getAddress().getHostAddress() + ":" + sourceContext.getPeerAddress().getPort() + ":" + request.getTokenString(); return tokenToSessionIdMap.remove(token); + } private String registerAsyncCoapSession(CoapExchange exchange, Request request, TransportProtos.SessionInfoProto sessionInfo, UUID sessionId) { - String token = request.getSource().getHostAddress() + ":" + request.getSourcePort() + ":" + request.getTokenString(); + EndpointContext sourceContext = request.getSourceContext(); + String token = sourceContext.getPeerAddress().getAddress().getHostAddress() + ":" + sourceContext.getPeerAddress().getPort() + ":" + request.getTokenString(); tokenToSessionIdMap.putIfAbsent(token, sessionInfo); CoapSessionListener attrListener = new CoapSessionListener(sessionId, exchange); transportService.registerAsyncSession(sessionInfo, attrListener); @@ -459,24 +468,17 @@ public class CoapTransportResource extends CoapResource { } } - public class CoapExchangeObserverProxy implements ExchangeObserver { - - private final ExchangeObserver proxy; - private final String token; - - CoapExchangeObserverProxy(ExchangeObserver proxy, String token) { - super(); - this.proxy = proxy; - this.token = token; - } - - @Override - public void completed(Exchange exchange) { - proxy.completed(exchange); - TransportProtos.SessionInfoProto session = tokenToSessionIdMap.remove(token); - if (session != null) { - closeAndDeregister(session); - } + public class CoapExchangeObserverProxy extends CoapObserveRelation { + + /** + * Constructs a new CoapObserveRelation with the specified request. + * + * @param request the request + * @param endpoint the endpoint + * @param executor + */ + protected CoapExchangeObserverProxy(Request request, Endpoint endpoint, ScheduledThreadPoolExecutor executor) { + super(request, endpoint, executor); } } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index c982f01603..8abc859b21 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -18,8 +18,9 @@ package org.thingsboard.server.transport.coap; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; + import org.eclipse.californium.core.network.CoapEndpoint; -import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.core.network.CoapEndpoint.Builder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; @@ -47,11 +48,15 @@ public class CoapTransportService { public void init() throws UnknownHostException { log.info("Starting CoAP transport..."); log.info("Starting CoAP transport server"); - this.server = new CoapServer(NetworkConfig.createStandardWithoutFile()); + this.server = new CoapServer(); createResources(); InetAddress addr = InetAddress.getByName(coapTransportContext.getHost()); InetSocketAddress sockAddr = new InetSocketAddress(addr, coapTransportContext.getPort()); - server.addEndpoint(new CoapEndpoint(sockAddr)); + Builder builder = new Builder(); + builder.setInetSocketAddress(sockAddr); + CoapEndpoint coapEndpoint = builder.build(); + + server.addEndpoint(coapEndpoint); server.start(); log.info("CoAP transport started!"); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index af391dedde..24429932cf 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.CoapTransportResource; + import java.util.Arrays; import java.util.HashSet; import java.util.List; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java index cf761efd3e..0fc5b44c43 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java @@ -28,6 +28,7 @@ import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapHandler; import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.elements.exception.ConnectorException; import org.thingsboard.server.common.msg.session.FeatureType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,7 +62,7 @@ public class DeviceEmulator { this.attributesClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.ATTRIBUTES)); this.telemetryClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.TELEMETRY)); this.rpcClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.RPC)); - this.keys = keys.split(","); + this.keys = (keys != null && !keys.isEmpty()) ? keys.split(",") : null; } public void start() { @@ -72,11 +73,8 @@ public class DeviceEmulator { try { sendObserveRequest(rpcClient); while (!Thread.interrupted()) { - - sendRequest(attributesClient, createAttributesRequest()); sendRequest(telemetryClient, createTelemetryRequest()); - Thread.sleep(1000); } } catch (Exception e) { @@ -84,8 +82,8 @@ public class DeviceEmulator { } } - private void sendRequest(CoapClient client, JsonNode request) throws JsonProcessingException { - CoapResponse telemetryResponse = client.setTimeout(60000).post(mapper.writeValueAsString(request), + private void sendRequest(CoapClient client, JsonNode request) throws IOException, ConnectorException { + CoapResponse telemetryResponse = client.setTimeout((long) 60000).post(mapper.writeValueAsString(request), MediaTypeRegistry.APPLICATION_JSON); log.info("Response: {}, {}", telemetryResponse.getCode(), telemetryResponse.getResponseText()); } @@ -113,6 +111,7 @@ public class DeviceEmulator { @Override public void onError() { + log.info("Command Response Ack Error, No connect"); //Do nothing } }, mapper.writeValueAsString(response), MediaTypeRegistry.APPLICATION_JSON); @@ -157,6 +156,15 @@ public class DeviceEmulator { if (args.length != 4) { System.out.println("Usage: java -jar " + DeviceEmulator.class.getSimpleName() + ".jar host port device_token keys"); } + /** + * DeviceEmulator(String host, int port, String token, String keys) + * args[]: + * host = "localhost", + * port = 0, + * token = "{Tokrn device from thingboard}"), kSzbDRGwaZqZ6Y25gTLF + * keys = "{Telemetry}" + * + */ final DeviceEmulator emulator = new DeviceEmulator(args[0], Integer.parseInt(args[1]), args[2], args[3]); emulator.start(); Runtime.getRuntime().addShutdownHook(new Thread() { @@ -167,7 +175,6 @@ public class DeviceEmulator { }); } - private String getFeatureTokenUrl(String host, int port, String token, FeatureType featureType) { return getBaseUrl(host, port) + token + "/" + featureType.name().toLowerCase(); } diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 6f5076adc0..f229627480 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.common.transport @@ -73,7 +73,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 2207d49dfc..e222b690ef 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -327,6 +327,7 @@ public class DeviceApiController { public void onToServerRpcResponse(ToServerRpcResponseMsg msg) { responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK)); } + } private void reportActivity(SessionInfoProto sessionInfo) { diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java index aad01c7c2f..13f38bfc80 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/HttpTransportContext.java @@ -17,9 +17,13 @@ package org.thingsboard.server.transport.http; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.http11.Http11NioProtocol; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; +import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.transport.TransportContext; @@ -37,4 +41,18 @@ public class HttpTransportContext extends TransportContext { @Value("${transport.http.request_timeout}") private long defaultTimeout; + @Getter + @Value("${transport.http.max_request_timeout}") + private long maxRequestTimeout; + + @Bean + public TomcatConnectorCustomizer tomcatAsyncTimeoutConnectorCustomizer() { + return connector -> { + ProtocolHandler handler = connector.getProtocolHandler(); + if (handler instanceof Http11NioProtocol) { + log.trace("Setting async max request timeout {}", maxRequestTimeout); + connector.setAsyncTimeout(maxRequestTimeout); + } + }; + } } diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml new file mode 100644 index 0000000000..fe19dae329 --- /dev/null +++ b/common/transport/lwm2m/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + org.thingsboard.common + 3.3.0-SNAPSHOT + transport + + org.thingsboard.common.transport + lwm2m + jar + + Thingsboard LwM2M Transport Common + https://thingsboard.io + + + UTF-8 + ${basedir}/../../.. + + + + + org.thingsboard.common.transport + transport-api + + + org.springframework + spring-context-support + + + org.springframework + spring-context + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.eclipse.leshan + leshan-server-cf + + + org.eclipse.leshan + leshan-client-cf + + + org.eclipse.leshan + leshan-server-redis + + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.eclipse.californium + californium-core + test-jar + test + + + org.eclipse.californium + californium-core + + + + + + + org.eclipse.californium + element-connector + test-jar + test + + + + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerConfiguration.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerConfiguration.java new file mode 100644 index 0000000000..757b91956b --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerConfiguration.java @@ -0,0 +1,289 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.core.model.StaticModel; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; +import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; +import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore; +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore; +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContextServer; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; + +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256; +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8; +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256; +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CCM_8; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.getCoapConfig; + +@Slf4j +@Component +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true'&& '${transport.lwm2m.bootstrap.enable:false}'=='true') || ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled:false}'=='true'&& '${transport.lwm2m.bootstrap.enable:false}'=='true')") +public class LwM2MTransportBootstrapServerConfiguration { + private PublicKey publicKey; + private PrivateKey privateKey; + private boolean pskMode = false; + + @Autowired + private LwM2MTransportContextBootstrap contextBs; + + @Autowired + private LwM2mTransportContextServer contextS; + + @Autowired + private LwM2MBootstrapSecurityStore lwM2MBootstrapSecurityStore; + + @Autowired + private LwM2MInMemoryBootstrapConfigStore lwM2MInMemoryBootstrapConfigStore; + + + @Bean + public LeshanBootstrapServer getLeshanBootstrapServer() { + log.info("Prepare and start BootstrapServer... PostConstruct"); + return this.getLhBootstrapServer(this.contextBs.getCtxBootStrap().getBootstrapPortNoSec(), this.contextBs.getCtxBootStrap().getBootstrapPortSecurity()); + } + + public LeshanBootstrapServer getLhBootstrapServer(Integer bootstrapPortNoSec, Integer bootstrapSecurePort) { + LeshanBootstrapServerBuilder builder = new LeshanBootstrapServerBuilder(); + builder.setLocalAddress(this.contextBs.getCtxBootStrap().getBootstrapHost(), bootstrapPortNoSec); + builder.setLocalSecureAddress(this.contextBs.getCtxBootStrap().getBootstrapHostSecurity(), bootstrapSecurePort); + + /** Create CoAP Config */ + builder.setCoapConfig(getCoapConfig(bootstrapPortNoSec, bootstrapSecurePort)); + + /** Define model provider (Create Models )*/ + builder.setModel(new StaticModel(contextS.getLwM2MTransportConfigServer().getModelsValue())); + + /** Create credentials */ + this.setServerWithCredentials(builder); + + /** Set securityStore with new ConfigStore */ + builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore); + + /** SecurityStore */ + builder.setSecurityStore(lwM2MBootstrapSecurityStore); + + + /** Create and Set DTLS Config */ + DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); + dtlsConfig.setRecommendedSupportedGroupsOnly(this.contextS.getLwM2MTransportConfigServer().isRecommendedSupportedGroups()); + dtlsConfig.setRecommendedCipherSuitesOnly(this.contextS.getLwM2MTransportConfigServer().isRecommendedCiphers()); + if (this.pskMode) { + dtlsConfig.setSupportedCipherSuites( + TLS_PSK_WITH_AES_128_CCM_8, + TLS_PSK_WITH_AES_128_CBC_SHA256); + } + else { + dtlsConfig.setSupportedCipherSuites( + TLS_PSK_WITH_AES_128_CCM_8, + TLS_PSK_WITH_AES_128_CBC_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); + } + + /** Set DTLS Config */ + builder.setDtlsConfig(dtlsConfig); + + BootstrapSessionManager sessionManager = new LwM2mDefaultBootstrapSessionManager(lwM2MBootstrapSecurityStore); + builder.setSessionManager(sessionManager); + + /** Create BootstrapServer */ + return builder.build(); + } + + private void setServerWithCredentials(LeshanBootstrapServerBuilder builder) { + try { + if (this.contextS.getLwM2MTransportConfigServer().getKeyStoreValue() != null) { + KeyStore keyStoreServer = this.contextS.getLwM2MTransportConfigServer().getKeyStoreValue(); + if (this.setBuilderX509(builder)) { + X509Certificate rootCAX509Cert = (X509Certificate) keyStoreServer.getCertificate(this.contextS.getLwM2MTransportConfigServer().getRootAlias()); + if (rootCAX509Cert != null) { + X509Certificate[] trustedCertificates = new X509Certificate[1]; + trustedCertificates[0] = rootCAX509Cert; + builder.setTrustedCertificates(trustedCertificates); + } else { + /** by default trust all */ + builder.setTrustedCertificates(new X509Certificate[0]); + } + } + } else if (this.setServerRPK(builder)) { + this.infoPramsUri("RPK"); + this.infoParamsBootstrapServerKey(this.publicKey, this.privateKey); + } else { + /** by default trust all */ + builder.setTrustedCertificates(new X509Certificate[0]); + log.info("Unable to load X509 files for BootStrapServer"); + this.pskMode = true; + this.infoPramsUri("PSK"); + } + } catch (KeyStoreException ex) { + log.error("[{}] Unable to load X509 files server", ex.getMessage()); + } + } + + private boolean setBuilderX509(LeshanBootstrapServerBuilder builder) { + /** + * For deb => KeyStorePathFile == yml or commandline: KEY_STORE_PATH_FILE + * For idea => KeyStorePathResource == common/transport/lwm2m/src/main/resources/credentials: in LwM2MTransportContextServer: credentials/serverKeyStore.jks + */ + try { + X509Certificate serverCertificate = (X509Certificate) this.contextS.getLwM2MTransportConfigServer().getKeyStoreValue().getCertificate(this.contextBs.getCtxBootStrap().getBootstrapAlias()); + PrivateKey privateKey = (PrivateKey) this.contextS.getLwM2MTransportConfigServer().getKeyStoreValue().getKey(this.contextBs.getCtxBootStrap().getBootstrapAlias(), this.contextS.getLwM2MTransportConfigServer().getKeyStorePasswordServer() == null ? null : this.contextS.getLwM2MTransportConfigServer().getKeyStorePasswordServer().toCharArray()); + PublicKey publicKey = serverCertificate.getPublicKey(); + if (serverCertificate != null && + privateKey != null && privateKey.getEncoded().length > 0 && + publicKey != null && publicKey.getEncoded().length > 0) { + builder.setPublicKey(serverCertificate.getPublicKey()); + builder.setPrivateKey(privateKey); + builder.setCertificateChain(new X509Certificate[]{serverCertificate}); + this.infoParamsServerX509(serverCertificate, publicKey, privateKey); + return true; + } else { + return false; + } + } catch (Exception ex) { + log.error("[{}] Unable to load KeyStore files server", ex.getMessage()); + return false; + } + } + + private void infoParamsServerX509(X509Certificate certificate, PublicKey publicKey, PrivateKey privateKey) { + try { + this.infoPramsUri("X509"); + log.info("\n- X509 Certificate (Hex): [{}]", + Hex.encodeHexString(certificate.getEncoded())); + this.infoParamsBootstrapServerKey(publicKey, privateKey); + } catch (CertificateEncodingException e) { + log.error("", e); + } + } + + private void infoPramsUri(String mode) { + log.info("Bootstrap Server uses [{}]: serverNoSecureURI : [{}], serverSecureURI : [{}]", + mode, + this.contextBs.getCtxBootStrap().getBootstrapHost() + ":" + this.contextBs.getCtxBootStrap().getBootstrapPortNoSec(), + this.contextBs.getCtxBootStrap().getBootstrapHostSecurity() + ":" + this.contextBs.getCtxBootStrap().getBootstrapPortSecurity()); + } + + + private boolean setServerRPK(LeshanBootstrapServerBuilder builder) { + try { + this.generateKeyForBootstrapRPK(); + if (this.publicKey != null && this.publicKey.getEncoded().length > 0 && + this.privateKey != null && this.privateKey.getEncoded().length > 0) { + builder.setPublicKey(this.publicKey); +// builder.setCertificateChain(new X509Certificate[] { serverCertificate }); + /// Trust all certificates. + builder.setTrustedCertificates(new X509Certificate[0]); + builder.setPrivateKey(this.privateKey); + return true; + } + } catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e) { + log.error("Fail create Bootstrap Server with RPK", e); + } + return false; + } + + + /** + * From yml: bootstrap + * public_x: "${LWM2M_SERVER_PUBLIC_X_BS:993ef2b698c6a9c0c1d8be78b13a9383c0854c7c7c7a504d289b403794648183}" + * public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:267412d5fc4e5ceb2257cb7fd7f76ebdac2fa9aa100afb162e990074cc0bfaa2}" + * private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:9dbdbb073fc63570693a9aaf1013414e261c571f27e27fc6a8c1c2ad9347875a}" + */ + private void generateKeyForBootstrapRPK() throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException { + /** Get Elliptic Curve Parameter spec for secp256r1 */ + AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); + algoParameters.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); + if (this.contextBs.getCtxBootStrap().getBootstrapPublicX() != null && !this.contextBs.getCtxBootStrap().getBootstrapPublicX().isEmpty() && this.contextBs.getCtxBootStrap().getBootstrapPublicY() != null && !this.contextBs.getCtxBootStrap().getBootstrapPublicY().isEmpty()) { + /** Get point values */ + byte[] publicX = Hex.decodeHex(this.contextBs.getCtxBootStrap().getBootstrapPublicX().toCharArray()); + byte[] publicY = Hex.decodeHex(this.contextBs.getCtxBootStrap().getBootstrapPublicY().toCharArray()); + /** Create key specs */ + KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), + parameterSpec); + /** Get public key */ + this.publicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); + } + if (this.contextBs.getCtxBootStrap().getBootstrapPrivateEncoded() != null && !this.contextBs.getCtxBootStrap().getBootstrapPrivateEncoded().isEmpty()) { + /** Get private key */ + byte[] privateS = Hex.decodeHex(this.contextBs.getCtxBootStrap().getBootstrapPrivateEncoded().toCharArray()); + try { + this.privateKey = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(privateS)); + } catch (InvalidKeySpecException ignore2) { + log.error("Invalid Bootstrap Server rpk.PrivateKey.getEncoded () [{}}]. PrivateKey has no EC algorithm", this.contextBs.getCtxBootStrap().getBootstrapPrivateEncoded()); + } + } + } + + private void infoParamsBootstrapServerKey(PublicKey publicKey, PrivateKey privateKey) { + /** Get x coordinate */ + byte[] x = ((ECPublicKey) publicKey).getW().getAffineX().toByteArray(); + if (x[0] == 0) + x = Arrays.copyOfRange(x, 1, x.length); + + /** Get Y coordinate */ + byte[] y = ((ECPublicKey) publicKey).getW().getAffineY().toByteArray(); + if (y[0] == 0) + y = Arrays.copyOfRange(y, 1, y.length); + + /** Get Curves params */ + String params = ((ECPublicKey) publicKey).getParams().toString(); + String privHex = Hex.encodeHexString(privateKey.getEncoded()); + log.info("\n- Public Key (Hex): [{}] \n" + + "- Private Key (Hex): [{}], \n" + + "public_x: \"${LWM2M_SERVER_PUBLIC_X_BS:{}}\" \n" + + "public_y: \"${LWM2M_SERVER_PUBLIC_Y_BS:{}}\" \n" + + "private_encoded: \"${LWM2M_SERVER_PRIVATE_ENCODED_BS:{}}\" \n" + + "- Elliptic Curve parameters : [{}]", + Hex.encodeHexString(publicKey.getEncoded()), + privHex, + Hex.encodeHexString(x), + Hex.encodeHexString(y), + privHex, + params); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerInitializer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerInitializer.java new file mode 100644 index 0000000000..c3d6d9d0c1 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapServerInitializer.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +@Slf4j +@Service +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true'&& '${transport.lwm2m.bootstrap.enable:false}'=='true') || ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled:false}'=='true'&& '${transport.lwm2m.bootstrap.enable:false}'=='true')") +public class LwM2MTransportBootstrapServerInitializer { + + @Autowired(required = false) + private LeshanBootstrapServer lhBServer; + + @Autowired + private LwM2MTransportContextBootstrap contextBS; + + @PostConstruct + public void init() { + this.lhBServer.start(); + } + + @PreDestroy + public void shutdown() throws InterruptedException { + log.info("Stopping LwM2M transport Bootstrap Server!"); + lhBServer.destroy(); + log.info("LwM2M transport Bootstrap Server stopped!"); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportContextBootstrap.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportContextBootstrap.java new file mode 100644 index 0000000000..43e3781716 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportContextBootstrap.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap; +/** + * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.TransportContext; +import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigBootstrap; + + +@Slf4j +@Component +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith'") +public class LwM2MTransportContextBootstrap extends TransportContext { + + private final LwM2MTransportConfigBootstrap lwM2MTransportConfigBootstrap; + + public LwM2MTransportContextBootstrap(LwM2MTransportConfigBootstrap ctxBootStrap) { + this.lwM2MTransportConfigBootstrap = ctxBootStrap; + } + + public LwM2MTransportConfigBootstrap getCtxBootStrap() { + return this.lwM2MTransportConfigBootstrap; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java new file mode 100644 index 0000000000..eba61af24c --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java @@ -0,0 +1,100 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap.secure; + +import lombok.Data; +import org.eclipse.leshan.core.SecurityMode; +import org.eclipse.leshan.core.request.BindingMode; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.server.bootstrap.BootstrapConfig; + +import java.nio.charset.StandardCharsets; + +@Data +public class LwM2MBootstrapConfig { + /** + * interface BootstrapSecurityConfig + * servers: BootstrapServersSecurityConfig, + * bootstrapServer: ServerSecurityConfig, + * lwm2mServer: ServerSecurityConfig + * } + */ + /** -servers + * shortId: number, + * lifetime: number, + * defaultMinPeriod: number, + * notifIfDisabled: boolean, + * binding: string + * */ + LwM2MBootstrapServers servers; + + /** -bootstrapServer, lwm2mServer + * interface ServerSecurityConfig + * host?: string, + * port?: number, + * isBootstrapServer?: boolean, + * securityMode: string, + * clientPublicKeyOrId?: string, + * clientSecretKey?: string, + * serverPublicKey?: string; + * clientHoldOffTime?: number, + * serverId?: number, + * bootstrapServerAccountTimeout: number + * */ + LwM2MServerBootstrap bootstrapServer; + + LwM2MServerBootstrap lwm2mServer; + + public BootstrapConfig getLwM2MBootstrapConfig() { + BootstrapConfig configBs = new BootstrapConfig(); + /** Delete old security objects */ + configBs.toDelete.add("/0"); + configBs.toDelete.add("/1"); + /** Server Configuration (object 1) as defined in LWM2M 1.0.x TS. */ + BootstrapConfig.ServerConfig server0 = new BootstrapConfig.ServerConfig(); + server0.shortId = servers.getShortId(); + server0.lifetime = servers.getLifetime(); + server0.defaultMinPeriod = servers.getDefaultMinPeriod(); + server0.notifIfDisabled = servers.isNotifIfDisabled(); + server0.binding = BindingMode.valueOf(servers.getBinding()); + configBs.servers.put(0, server0); + /** Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */ + this.bootstrapServer.setBootstrapServerIs(true); + configBs.security.put(0, setServerSecuruty(this.bootstrapServer.getHost(), this.bootstrapServer.getPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); + /** Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */ + configBs.security.put(1, setServerSecuruty(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); + return configBs; + } + + private BootstrapConfig.ServerSecurity setServerSecuruty(String host, Integer port, boolean bootstrapServer, String securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { + BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity(); + serverSecurity.uri = "coaps://" + host + ":" + Integer.toString(port); + serverSecurity.bootstrapServer = bootstrapServer; + serverSecurity.securityMode = SecurityMode.valueOf(securityMode); + serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode); + serverSecurity.serverPublicKey = (serverPublicKey != null && !serverPublicKey.isEmpty()) ? Hex.decodeHex(serverPublicKey.toCharArray()) : new byte[]{}; + serverSecurity.secretKey = (secretKey != null && !secretKey.isEmpty()) ? Hex.decodeHex(secretKey.toCharArray()) : new byte[]{}; + serverSecurity.serverId = serverId; + return serverSecurity; + } + + private byte[] setPublicKeyOrId(String publicKeyOrIdStr, String securityMode) { + byte[] publicKey = (publicKeyOrIdStr == null || publicKeyOrIdStr.isEmpty()) ? new byte[]{} : + SecurityMode.valueOf(securityMode).equals(SecurityMode.PSK) ? publicKeyOrIdStr.getBytes(StandardCharsets.UTF_8) : + Hex.decodeHex(publicKeyOrIdStr.toCharArray()); + return publicKey; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java new file mode 100644 index 0000000000..8e266a2638 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java @@ -0,0 +1,201 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap.secure; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.SecurityMode; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.core.util.SecurityUtil; +import org.eclipse.leshan.server.bootstrap.BootstrapConfig; +import org.eclipse.leshan.server.bootstrap.EditableBootstrapConfigStore; +import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; +import org.eclipse.leshan.server.security.BootstrapSecurityStore; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Service; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; +import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; +import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; +import org.thingsboard.server.transport.lwm2m.server.LwM2mSessionMsgListener; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContextServer; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; +import org.thingsboard.server.transport.lwm2m.utils.TypeServer; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.BOOTSTRAP_SERVER; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_ERROR; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_INFO; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LWM2M_SERVER; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.SERVERS; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.getBootstrapParametersFromThingsboard; + +@Slf4j +@Service("LwM2MBootstrapSecurityStore") +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true' && '${transport.lwm2m.bootstrap.enable:false}'=='true') || ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled}'=='true' && '${transport.lwm2m.bootstrap.enable}'=='true')") +public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore { + + private final EditableBootstrapConfigStore bootstrapConfigStore; + + private final LwM2mCredentialsSecurityInfoValidator lwM2MCredentialsSecurityInfoValidator; + + private final LwM2mTransportContextServer context; + + public LwM2MBootstrapSecurityStore(EditableBootstrapConfigStore bootstrapConfigStore, LwM2mCredentialsSecurityInfoValidator lwM2MCredentialsSecurityInfoValidator, LwM2mTransportContextServer context) { + this.bootstrapConfigStore = bootstrapConfigStore; + this.lwM2MCredentialsSecurityInfoValidator = lwM2MCredentialsSecurityInfoValidator; + this.context = context; + } + + @Override + public List getAllByEndpoint(String endPoint) { + String endPointKey = endPoint; + ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPointKey, TypeServer.BOOTSTRAP); + if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { + /** add value to store from BootstrapJson */ + this.setBootstrapConfigScurityInfo(store); + BootstrapConfig bsConfigNew = store.getBootstrapConfig(); + if (bsConfigNew != null) { + try { + for (String config : bootstrapConfigStore.getAll().keySet()) { + if (config.equals(endPoint)) { + bootstrapConfigStore.remove(config); + } + } + bootstrapConfigStore.add(endPoint, bsConfigNew); + } catch (InvalidConfigurationException e) { + log.error("", e); + } + return store.getSecurityInfo() == null ? null : Arrays.asList(store.getSecurityInfo()); + } + } + return null; + } + + @Override + public SecurityInfo getByIdentity(String identity) { + ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(identity, TypeServer.BOOTSTRAP); + if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { + /** add value to store from BootstrapJson */ + this.setBootstrapConfigScurityInfo(store); + BootstrapConfig bsConfig = store.getBootstrapConfig(); + if (bsConfig.security != null) { + try { + bootstrapConfigStore.add(store.getEndPoint(), bsConfig); + } catch (InvalidConfigurationException e) { + log.error("", e); + } + return store.getSecurityInfo(); + } + } + return null; + } + + private void setBootstrapConfigScurityInfo(ReadResultSecurityStore store) { + /** BootstrapConfig */ + LwM2MBootstrapConfig lwM2MBootstrapConfig = this.getParametersBootstrap(store); + if (lwM2MBootstrapConfig != null) { + /** Security info */ + switch (SecurityMode.valueOf(lwM2MBootstrapConfig.getBootstrapServer().getSecurityMode())) { + /** Use RPK only */ + case PSK: + store.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(store.getEndPoint(), + lwM2MBootstrapConfig.getBootstrapServer().getClientPublicKeyOrId(), + Hex.decodeHex(lwM2MBootstrapConfig.getBootstrapServer().getClientSecretKey().toCharArray()))); + store.setSecurityMode(SecurityMode.PSK.code); + break; + case RPK: + try { + store.setSecurityInfo(SecurityInfo.newRawPublicKeyInfo(store.getEndPoint(), + SecurityUtil.publicKey.decode(Hex.decodeHex(lwM2MBootstrapConfig.getBootstrapServer().getClientPublicKeyOrId().toCharArray())))); + store.setSecurityMode(SecurityMode.RPK.code); + break; + } catch (IOException | GeneralSecurityException e) { + log.error("Unable to decode Client public key for [{}] [{}]", store.getEndPoint(), e.getMessage()); + } + case X509: + store.setSecurityInfo(SecurityInfo.newX509CertInfo(store.getEndPoint())); + store.setSecurityMode(SecurityMode.X509.code); + break; + case NO_SEC: + store.setSecurityMode(SecurityMode.NO_SEC.code); + store.setSecurityInfo(null); + break; + default: + } + BootstrapConfig bootstrapConfig = lwM2MBootstrapConfig.getLwM2MBootstrapConfig(); + store.setBootstrapConfig(bootstrapConfig); + } + } + + private LwM2MBootstrapConfig getParametersBootstrap(ReadResultSecurityStore store) { + try { + JsonObject bootstrapJsonCredential = store.getBootstrapJsonCredential(); + if (bootstrapJsonCredential != null) { + ObjectMapper mapper = new ObjectMapper(); + LwM2MBootstrapConfig lwM2MBootstrapConfig = mapper.readValue(bootstrapJsonCredential.toString(), LwM2MBootstrapConfig.class); + JsonObject bootstrapObject = getBootstrapParametersFromThingsboard(store.getDeviceProfile()); + lwM2MBootstrapConfig.servers = mapper.readValue(bootstrapObject.get(SERVERS).toString(), LwM2MBootstrapServers.class); + LwM2MServerBootstrap profileServerBootstrap = mapper.readValue(bootstrapObject.get(BOOTSTRAP_SERVER).toString(), LwM2MServerBootstrap.class); + LwM2MServerBootstrap profileLwm2mServer = mapper.readValue(bootstrapObject.get(LWM2M_SERVER).toString(), LwM2MServerBootstrap.class); + UUID sessionUUiD = UUID.randomUUID(); + TransportProtos.SessionInfoProto sessionInfo = context.getValidateSessionInfo(store.getMsg(), sessionUUiD.getMostSignificantBits(), sessionUUiD.getLeastSignificantBits()); + context.getTransportService().registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(null, sessionInfo)); + if (this.getValidatedSecurityMode(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap, lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer)) { + lwM2MBootstrapConfig.bootstrapServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.bootstrapServer, profileServerBootstrap); + lwM2MBootstrapConfig.lwm2mServer = new LwM2MServerBootstrap(lwM2MBootstrapConfig.lwm2mServer, profileLwm2mServer); + String logMsg = String.format("%s: getParametersBootstrap: %s Access connect client with bootstrap server.", LOG_LW2M_INFO, store.getEndPoint()); + context.sentParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + return lwM2MBootstrapConfig; + } else { + log.error(" [{}] Different values SecurityMode between of client and profile.", store.getEndPoint()); + log.error("{} getParametersBootstrap: [{}] Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); + String logMsg = String.format("%s: getParametersBootstrap: %s Different values SecurityMode between of client and profile.", LOG_LW2M_ERROR, store.getEndPoint()); + context.sentParametersOnThingsboard(context.getTelemetryMsgObject(logMsg), LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC, sessionInfo); + return null; + } + } + } catch (JsonProcessingException e) { + log.error("Unable to decode Json or Certificate for [{}] [{}]", store.getEndPoint(), e.getMessage()); + return null; + } + log.error("Unable to decode Json or Certificate for [{}]", store.getEndPoint()); + return null; + } + + /** + * Bootstrap security have to sync between (bootstrapServer in credential and bootstrapServer in profile) + * and (lwm2mServer in credential and lwm2mServer in profile + * + * @param bootstrapFromCredential - Bootstrap -> Security of bootstrapServer in credential + * @param profileServerBootstrap - Bootstrap -> Security of bootstrapServer in profile + * @param lwm2mFromCredential - Bootstrap -> Security of lwm2mServer in credential + * @param profileLwm2mServer - Bootstrap -> Security of lwm2mServer in profile + * @return false if not sync between SecurityMode of Bootstrap credential and profile + */ + private boolean getValidatedSecurityMode(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap, LwM2MServerBootstrap lwm2mFromCredential, LwM2MServerBootstrap profileLwm2mServer) { + return (bootstrapFromCredential.getSecurityMode().equals(profileServerBootstrap.getSecurityMode()) && + lwm2mFromCredential.getSecurityMode().equals(profileLwm2mServer.getSecurityMode())); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java new file mode 100644 index 0000000000..e3757701cb --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapServers.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap.secure; + +import lombok.Data; + +@Data +public class LwM2MBootstrapServers { + private Integer shortId = 123; + private Integer lifetime = 300; + private Integer defaultMinPeriod = 1; + private boolean notifIfDisabled = true; + private String binding = "U"; +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MInMemoryBootstrapConfigStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MInMemoryBootstrapConfigStore.java new file mode 100644 index 0000000000..b20954cf7d --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MInMemoryBootstrapConfigStore.java @@ -0,0 +1,71 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap.secure; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.server.bootstrap.BootstrapConfig; +import org.eclipse.leshan.server.bootstrap.InMemoryBootstrapConfigStore; +import org.eclipse.leshan.server.bootstrap.InvalidConfigurationException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Slf4j +@Component("LwM2MInMemoryBootstrapConfigStore") +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true' && '${transport.lwm2m.bootstrap.enable:false}'=='true') || ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled}'=='true'&& '${transport.lwm2m.bootstrap.enable}'=='true')") +public class LwM2MInMemoryBootstrapConfigStore extends InMemoryBootstrapConfigStore { + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final Lock readLock = readWriteLock.readLock(); + private final Lock writeLock = readWriteLock.writeLock(); + + @Override + public Map getAll() { + readLock.lock(); + try { + return super.getAll(); + } finally { + readLock.unlock(); + } + } + + @Override + public void add(String endpoint, BootstrapConfig config) throws InvalidConfigurationException { + writeLock.lock(); + try { + addToStore(endpoint, config); + } finally { + writeLock.unlock(); + } + } + + @Override + public BootstrapConfig remove(String enpoint) { + writeLock.lock(); + try { + BootstrapConfig res = super.remove(enpoint); + return res; + } finally { + writeLock.unlock(); + } + } + + public void addToStore(String endpoint, BootstrapConfig config) throws InvalidConfigurationException { + super.add(endpoint, config); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java new file mode 100644 index 0000000000..9dca6057da --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MServerBootstrap.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap.secure; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.SecurityMode; + +@Slf4j +@Data +public class LwM2MServerBootstrap { + + String clientPublicKeyOrId = ""; + String clientSecretKey = ""; + String serverPublicKey = ""; + Integer clientHoldOffTime = 1; + Integer bootstrapServerAccountTimeout = 0; + + String host = "0.0.0.0"; + Integer port = 0; + + String securityMode = SecurityMode.NO_SEC.name(); + + Integer serverId = 123; + boolean bootstrapServerIs = false; + + public LwM2MServerBootstrap(){}; + + public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) { + this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId(); + this.clientSecretKey = bootstrapFromCredential.getClientSecretKey(); + this.serverPublicKey = profileServerBootstrap.getServerPublicKey(); + this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime(); + this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout(); + this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost(); + this.port = profileServerBootstrap.getPort(); + this.securityMode = profileServerBootstrap.getSecurityMode(); + this.serverId = profileServerBootstrap.getServerId(); + this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java new file mode 100644 index 0000000000..34497069f5 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2mDefaultBootstrapSessionManager.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.bootstrap.secure; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.request.Identity; +import org.eclipse.leshan.server.bootstrap.BootstrapSession; +import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSession; +import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSessionManager; +import org.eclipse.leshan.server.security.BootstrapSecurityStore; +import org.eclipse.leshan.server.security.SecurityChecker; +import org.eclipse.leshan.server.security.SecurityInfo; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Slf4j +public class LwM2mDefaultBootstrapSessionManager extends DefaultBootstrapSessionManager { + + private BootstrapSecurityStore bsSecurityStore; + private SecurityChecker securityChecker; + + /** + * Create a {@link DefaultBootstrapSessionManager} using a default {@link SecurityChecker} to accept or refuse new + * {@link BootstrapSession}. + * + * @param bsSecurityStore the {@link BootstrapSecurityStore} used by default {@link SecurityChecker}. + */ + public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore) { + this(bsSecurityStore, new SecurityChecker()); + } + + public LwM2mDefaultBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore, SecurityChecker securityChecker) { + super(bsSecurityStore); + this.bsSecurityStore = bsSecurityStore; + this.securityChecker = securityChecker; + } + + @SuppressWarnings("deprecation") + public BootstrapSession begin(String endpoint, Identity clientIdentity) { + boolean authorized; + if (bsSecurityStore != null) { + List securityInfos = (clientIdentity.getPskIdentity() != null && !clientIdentity.getPskIdentity().isEmpty()) ? Collections.singletonList(bsSecurityStore.getByIdentity(clientIdentity.getPskIdentity())) : bsSecurityStore.getAllByEndpoint(endpoint); + log.info("Bootstrap session started securityInfos: [{}]", securityInfos); + authorized = securityChecker.checkSecurityInfos(endpoint, clientIdentity, securityInfos); + } else { + authorized = true; + } + DefaultBootstrapSession session = new DefaultBootstrapSession(endpoint, clientIdentity, authorized); + log.info("Bootstrap session started : {}", session); + return session; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LWM2MGenerationPSkRPkECC.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LWM2MGenerationPSkRPkECC.java new file mode 100644 index 0000000000..4df520543b --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LWM2MGenerationPSkRPkECC.java @@ -0,0 +1,121 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.secure; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.util.Hex; +import java.security.SecureRandom; +import java.security.KeyPairGenerator; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.InvalidAlgorithmParameterException; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; + +@Slf4j +public class LWM2MGenerationPSkRPkECC { + + public LWM2MGenerationPSkRPkECC(Integer dtlsMode) { + switch (LwM2MSecurityMode.fromSecurityMode(dtlsMode)) { + case PSK: + generationPSkKey(); + break; + case RPK: + generationRPKECCKey(); + } + } + + public LWM2MGenerationPSkRPkECC() { + generationPSkKey(); + generationRPKECCKey(); + } + + private void generationPSkKey() { + /** PSK */ + int lenPSkKey = 32; + /** Start PSK + * Clients and Servers MUST support PSK keys of up to 64 bytes in length, as required by [RFC7925] + * SecureRandom object must be unpredictable, and all SecureRandom output sequences must be cryptographically strong, as described in [RFC4086] + * */ + SecureRandom randomPSK = new SecureRandom(); + byte bytesPSK[] = new byte[lenPSkKey]; + randomPSK.nextBytes(bytesPSK); + log.info("\nCreating new PSK: \n for the next start PSK -> security key: [{}]", Hex.encodeHexString(bytesPSK)); + } + + private void generationRPKECCKey() { + /** RPK */ + String algorithm = "EC"; + String provider = "SunEC"; + String nameParameterSpec = "secp256r1"; + + /** Start RPK + * Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] + * */ + KeyPairGenerator kpg = null; + try { + kpg = KeyPairGenerator.getInstance(algorithm, provider); + } catch (NoSuchAlgorithmException e) { + log.error("", e); + } catch (NoSuchProviderException e) { + log.error("", e); + } + ECGenParameterSpec ecsp = new ECGenParameterSpec(nameParameterSpec); + try { + kpg.initialize(ecsp); + } catch (InvalidAlgorithmParameterException e) { + log.error("", e); + } + + KeyPair kp = kpg.genKeyPair(); + PrivateKey privKey = kp.getPrivate(); + PublicKey pubKey = kp.getPublic(); + + if (pubKey instanceof ECPublicKey) { + ECPublicKey ecPublicKey = (ECPublicKey) pubKey; + /** Get x coordinate */ + byte[] x = ecPublicKey.getW().getAffineX().toByteArray(); + if (x[0] == 0) + x = Arrays.copyOfRange(x, 1, x.length); + + /** Get Y coordinate */ + byte[] y = ecPublicKey.getW().getAffineY().toByteArray(); + if (y[0] == 0) + y = Arrays.copyOfRange(y, 1, y.length); + + /** Get Curves params */ + String privHex = Hex.encodeHexString(privKey.getEncoded()); + log.info("\nCreating new RPK for the next start... \n" + + " Public Key (Hex): [{}]\n" + + " Private Key (Hex): [{}]" + + " public_x : [{}] \n" + + " public_y : [{}] \n" + + " private_encode : [{}] \n" + + " Elliptic Curve parameters : [{}] \n", + Hex.encodeHexString(pubKey.getEncoded()), + privHex, + Hex.encodeHexString(x), + Hex.encodeHexString(y), + privHex, + ecPublicKey.getParams().toString()); + } + } +} + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2MSecurityMode.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2MSecurityMode.java new file mode 100644 index 0000000000..faf776b76c --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2MSecurityMode.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.secure; + +public enum LwM2MSecurityMode { + + PSK(0, "psk"), + RPK(1, "rpk"), + X509(2, "x509"), + NO_SEC(3, "no_sec"), + X509_EST(4, "x509_est"), + REDIS(7, "redis"), + DEFAULT_MODE(255, "default_mode"); + + public int code; + public String subEndpoint; + + LwM2MSecurityMode(int code, String subEndpoint) { + this.code = code; + this.subEndpoint = subEndpoint; + } + + public static LwM2MSecurityMode fromSecurityMode(long code) { + return fromSecurityMode((int) code); + } + + public static LwM2MSecurityMode fromSecurityMode(int code) { + for (LwM2MSecurityMode sm : LwM2MSecurityMode.values()) { + if (sm.code == code) { + return sm; + } + } + throw new IllegalArgumentException(String.format("Unsupported security code : %d", code)); + } + + + public static LwM2MSecurityMode fromSecurityMode(String subEndpoint) { + for (LwM2MSecurityMode sm : LwM2MSecurityMode.values()) { + if (sm.subEndpoint.equals(subEndpoint)) { + return sm; + } + } + throw new IllegalArgumentException(String.format("Unsupported security subEndpoint : %d", subEndpoint)); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java new file mode 100644 index 0000000000..86a333e361 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mCredentialsSecurityInfoValidator.java @@ -0,0 +1,184 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.secure; + +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.core.util.SecurityUtil; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContextServer; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; +import org.thingsboard.server.transport.lwm2m.utils.TypeServer; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; +import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.PSK; +import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.RPK; +import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.X509; + +@Slf4j +@Component +@TbLwM2mTransportComponent +public class LwM2mCredentialsSecurityInfoValidator { + + private final LwM2mTransportContextServer contextS; + + public LwM2mCredentialsSecurityInfoValidator(LwM2mTransportContextServer contextS) { + this.contextS = contextS; + } + + /** + * Request to thingsboard Response from thingsboard ValidateDeviceLwM2MCredentials + * @param endpoint - + * @param keyValue - + * @return ValidateDeviceCredentialsResponseMsg and SecurityInfo + */ + public ReadResultSecurityStore createAndValidateCredentialsSecurityInfo(String endpoint, TypeServer keyValue) { + CountDownLatch latch = new CountDownLatch(1); + final ReadResultSecurityStore[] resultSecurityStore = new ReadResultSecurityStore[1]; + contextS.getTransportService().process(ValidateDeviceLwM2MCredentialsRequestMsg.newBuilder().setCredentialsId(endpoint).build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponseMsg msg) { + String credentialsBody = msg.getCredentialsBody(); + resultSecurityStore[0] = createSecurityInfo(endpoint, credentialsBody, keyValue); + resultSecurityStore[0].setMsg(msg); + Optional deviceProfileOpt = LwM2mTransportHandler.decode(msg.getProfileBody().toByteArray()); + deviceProfileOpt.ifPresent(profile -> resultSecurityStore[0].setDeviceProfile(profile)); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] [{}] Failed to process credentials ", endpoint, e); + resultSecurityStore[0] = createSecurityInfo(endpoint, null, null); + latch.countDown(); + } + }); + try { + latch.await(contextS.getLwM2MTransportConfigServer().getTimeout(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + log.error("Failed to await credentials!", e); + } + return resultSecurityStore[0]; + } + + /** + * Create new SecurityInfo + * @param endPoint - + * @param jsonStr - + * @param keyValue - + * @return SecurityInfo + */ + private ReadResultSecurityStore createSecurityInfo(String endPoint, String jsonStr, TypeServer keyValue) { + ReadResultSecurityStore result = new ReadResultSecurityStore(); + JsonObject objectMsg = LwM2mTransportHandler.validateJson(jsonStr); + if (objectMsg != null && !objectMsg.isJsonNull()) { + JsonObject object = (objectMsg.has(keyValue.type) && !objectMsg.get(keyValue.type).isJsonNull()) ? objectMsg.get(keyValue.type).getAsJsonObject() : null; + /** + * Only PSK + */ + String endPointPsk = (objectMsg.has("client") + && objectMsg.get("client").getAsJsonObject().has("endpoint") + && objectMsg.get("client").getAsJsonObject().get("endpoint").isJsonPrimitive()) ? objectMsg.get("client").getAsJsonObject().get("endpoint").getAsString() : null; + endPoint = (endPointPsk == null || endPointPsk.isEmpty()) ? endPoint : endPointPsk; + if (object != null && !object.isJsonNull()) { + if (keyValue.equals(TypeServer.BOOTSTRAP)) { + result.setBootstrapJsonCredential(object); + result.setEndPoint(endPoint); + result.setSecurityMode(LwM2MSecurityMode.fromSecurityMode(object.get("bootstrapServer").getAsJsonObject().get("securityMode").getAsString().toLowerCase()).code); + } else { + LwM2MSecurityMode lwM2MSecurityMode = LwM2MSecurityMode.fromSecurityMode(object.get("securityConfigClientMode").getAsString().toLowerCase()); + switch (lwM2MSecurityMode) { + case NO_SEC: + createClientSecurityInfoNoSec(result); + break; + case PSK: + createClientSecurityInfoPSK(result, endPoint, object); + break; + case RPK: + createClientSecurityInfoRPK(result, endPoint, object); + break; + case X509: + createClientSecurityInfoX509(result, endPoint); + break; + default: + break; + } + } + } + } + return result; + } + + private void createClientSecurityInfoNoSec(ReadResultSecurityStore result) { + result.setSecurityInfo(null); + result.setSecurityMode(NO_SEC.code); + } + + private void createClientSecurityInfoPSK(ReadResultSecurityStore result, String endPoint, JsonObject object) { + /** PSK Deserialization */ + String identity = (object.has("identity") && object.get("identity").isJsonPrimitive()) ? object.get("identity").getAsString() : null; + if (identity != null && !identity.isEmpty()) { + try { + byte[] key = (object.has("key") && object.get("key").isJsonPrimitive()) ? Hex.decodeHex(object.get("key").getAsString().toCharArray()) : null; + if (key != null && key.length > 0) { + if (endPoint != null && !endPoint.isEmpty()) { + result.setSecurityInfo(SecurityInfo.newPreSharedKeyInfo(endPoint, identity, key)); + result.setSecurityMode(PSK.code); + } + } + } catch (IllegalArgumentException e) { + log.error("Missing PSK key: " + e.getMessage()); + } + } else { + log.error("Missing PSK identity"); + } + } + + private void createClientSecurityInfoRPK(ReadResultSecurityStore result, String endpoint, JsonObject object) { + try { + if (object.has("key") && object.get("key").isJsonPrimitive()) { + byte[] rpkkey = Hex.decodeHex(object.get("key").getAsString().toLowerCase().toCharArray()); + PublicKey key = SecurityUtil.publicKey.decode(rpkkey); + result.setSecurityInfo(SecurityInfo.newRawPublicKeyInfo(endpoint, key)); + result.setSecurityMode(RPK.code); + } else { + log.error("Missing RPK key"); + } + } catch (IllegalArgumentException | IOException | GeneralSecurityException e) { + log.error("RPK: Invalid security info content: " + e.getMessage()); + } + } + + private void createClientSecurityInfoX509(ReadResultSecurityStore result, String endpoint) { + result.setSecurityInfo(SecurityInfo.newX509CertInfo(endpoint)); + result.setSecurityMode(X509.code); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mRPkCredentials.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mRPkCredentials.java new file mode 100644 index 0000000000..42ed0dbee2 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/LwM2mRPkCredentials.java @@ -0,0 +1,84 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.secure; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.util.Hex; +import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.AlgorithmParameters; +import java.security.KeyFactory; +import java.security.GeneralSecurityException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.KeySpec; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPoint; +import java.util.List; + +@Slf4j +@Data +public class LwM2mRPkCredentials { + private PublicKey serverPublicKey; + private PrivateKey serverPrivateKey; + private X509Certificate certificate; + private List trustStore; + + /** + * create All key RPK credentials + * @param publX + * @param publY + * @param privS + */ + public LwM2mRPkCredentials(String publX, String publY, String privS) { + generatePublicKeyRPK(publX, publY, privS); + } + + private void generatePublicKeyRPK(String publX, String publY, String privS) { + try { + /**Get Elliptic Curve Parameter spec for secp256r1 */ + AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); + algoParameters.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); + if (publX != null && !publX.isEmpty() && publY != null && !publY.isEmpty()) { + // Get point values + byte[] publicX = Hex.decodeHex(publX.toCharArray()); + byte[] publicY = Hex.decodeHex(publY.toCharArray()); + /** Create key specs */ + KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), + parameterSpec); + /** Get keys */ + this.serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); + } + if (privS != null && !privS.isEmpty()) { + /** Get point values */ + byte[] privateS = Hex.decodeHex(privS.toCharArray()); + /** Create key specs */ + KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); + /** Get keys */ + this.serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); + } + } catch (GeneralSecurityException | IllegalArgumentException e) { + log.error("[{}] Failed generate Server KeyRPK", e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/ReadResultSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/ReadResultSecurityStore.java new file mode 100644 index 0000000000..d12247467f --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/ReadResultSecurityStore.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.secure; + +import com.google.gson.JsonObject; +import lombok.Data; +import org.eclipse.leshan.server.bootstrap.BootstrapConfig; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; + +import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.DEFAULT_MODE; + +@Data +public class ReadResultSecurityStore { + private ValidateDeviceCredentialsResponseMsg msg; + private SecurityInfo securityInfo; + private int securityMode = DEFAULT_MODE.code; + + /** bootstrap */ + DeviceProfile deviceProfile; + JsonObject bootstrapJsonCredential; + String endPoint; + BootstrapConfig bootstrapConfig; +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java new file mode 100644 index 0000000000..f89f7ec952 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mServerListener.java @@ -0,0 +1,112 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.response.ObserveResponse; +import org.eclipse.leshan.server.observation.ObservationListener; +import org.eclipse.leshan.server.queue.PresenceListener; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationListener; +import org.eclipse.leshan.server.registration.RegistrationUpdate; + +import java.util.Collection; + +@Slf4j +public class LwM2mServerListener { + + private final LwM2mTransportServiceImpl service; + + public LwM2mServerListener(LwM2mTransportServiceImpl service) { + this.service = service; + } + + public final RegistrationListener registrationListener = new RegistrationListener() { + /** + * Register – запрос, представленный в виде POST /rd?… + */ + @Override + public void registered(Registration registration, Registration previousReg, + Collection previousObservations) { + service.onRegistered(registration, previousObservations); + } + + /** + * Update – представляет из себя CoAP POST запрос на URL, полученный в ответ на Register. + */ + @Override + public void updated(RegistrationUpdate update, Registration updatedRegistration, + Registration previousRegistration) { + service.updatedReg(updatedRegistration); + } + + /** + * De-register (CoAP DELETE) – отправляется клиентом в случае инициирования процедуры выключения. + */ + @Override + public void unregistered(Registration registration, Collection observations, boolean expired, + Registration newReg) { + service.unReg(registration, observations); + } + + }; + + public final PresenceListener presenceListener = new PresenceListener() { + @Override + public void onSleeping(Registration registration) { + log.info("onSleeping"); + service.onSleepingDev(registration); + } + + @Override + public void onAwake(Registration registration) { + log.info("onAwake"); + service.onAwakeDev(registration); + } + }; + + public final ObservationListener observationListener = new ObservationListener() { + + @Override + public void cancelled(Observation observation) { + log.info("Received notification cancelled from [{}] ", observation.getPath()); + } + + @Override + public void onResponse(Observation observation, Registration registration, ObserveResponse response) { + if (registration != null) { + try { + service.onObservationResponse(registration, observation.getPath().toString(), response); + } catch (Exception e) { + log.error("[{}] onResponse", e.toString()); + + } + } + } + + @Override + public void onError(Observation observation, Registration registration, Exception error) { + log.error(String.format("Unable to handle notification of [%s:%s]", observation.getRegistrationId(), observation.getPath()), error); + } + + @Override + public void newObservation(Observation observation, Registration registration) { +// log.info("Received newObservation from [{}] endpoint [{}] ", observation.getPath(), registration.getEndpoint()); + } + }; + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java new file mode 100644 index 0000000000..15fb015f32 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -0,0 +1,88 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.SessionMsgListener; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; + +import java.util.Optional; + +@Slf4j +public class LwM2mSessionMsgListener implements GenericFutureListener>, SessionMsgListener { + private LwM2mTransportServiceImpl service; + private TransportProtos.SessionInfoProto sessionInfo; + + public LwM2mSessionMsgListener(LwM2mTransportServiceImpl service, TransportProtos.SessionInfoProto sessionInfo) { + this.service = service; + this.sessionInfo = sessionInfo; + } + + @Override + public void onGetAttributesResponse(GetAttributeResponseMsg getAttributesResponse) { + this.service.onGetAttributesResponse(getAttributesResponse, this.sessionInfo); + } + + @Override + public void onAttributeUpdate(AttributeUpdateNotificationMsg attributeUpdateNotification) { + this.service.onAttributeUpdate(attributeUpdateNotification, this.sessionInfo); + } + + @Override + public void onRemoteSessionCloseCommand(SessionCloseNotificationProto sessionCloseNotification) { + log.info("[{}] sessionCloseNotification", sessionCloseNotification); + } + + @Override + public void onToTransportUpdateCredentials(ToTransportUpdateCredentialsProto updateCredentials) { + this.service.onToTransportUpdateCredentials(updateCredentials); + } + + @Override + public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { + this.service.onDeviceProfileUpdate(sessionInfo, deviceProfile); + } + + @Override + public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { + this.service.onDeviceUpdate(sessionInfo, device, deviceProfileOpt); + } + + @Override + public void onToDeviceRpcRequest(ToDeviceRpcRequestMsg toDeviceRequest) { + log.info("[{}] toDeviceRpcRequest", toDeviceRequest); + } + + @Override + public void onToServerRpcResponse(ToServerRpcResponseMsg toServerResponse) { + log.info("[{}] toServerRpcResponse", toServerResponse); + } + + @Override + public void operationComplete(Future future) throws Exception { + log.info("[{}] operationComplete", future); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java new file mode 100644 index 0000000000..062c0f1148 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportContextServer.java @@ -0,0 +1,134 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; +/** + * Copyright © 2016-2020 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 com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.TransportContext; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.lwm2m.LwM2MTransportConfigServer; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.adaptors.LwM2MJsonAdaptor; + +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_TELEMETRY; + +@Slf4j +@Component +@TbLwM2mTransportComponent +public class LwM2mTransportContextServer extends TransportContext { + + + private final LwM2MTransportConfigServer lwM2MTransportConfigServer; + + private final TransportService transportService; + + @Getter + private final LwM2MJsonAdaptor adaptor; + + public LwM2mTransportContextServer(LwM2MTransportConfigServer lwM2MTransportConfigServer, TransportService transportService, LwM2MJsonAdaptor adaptor) { + this.lwM2MTransportConfigServer = lwM2MTransportConfigServer; + this.transportService = transportService; + this.adaptor = adaptor; + } + + public LwM2MTransportConfigServer getLwM2MTransportConfigServer() { + return this.lwM2MTransportConfigServer; + } + + /** + * Sent to Thingsboard Attribute || Telemetry + * + * @param msg - JsonObject: [{name: value}] + * @return - dummy + */ + private TransportServiceCallback getPubAckCallbackSentAttrTelemetry(final T msg) { + return new TransportServiceCallback<>() { + @Override + public void onSuccess(Void dummy) { + log.trace("Success to publish msg: {}, dummy: {}", msg, dummy); + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to publish msg: {}", msg, e); + } + }; + } + + public void sentParametersOnThingsboard(JsonElement msg, String topicName, TransportProtos.SessionInfoProto sessionInfo) { + try { + if (topicName.equals(LwM2mTransportHandler.DEVICE_ATTRIBUTES_TOPIC)) { + TransportProtos.PostAttributeMsg postAttributeMsg = adaptor.convertToPostAttributes(msg); + TransportServiceCallback call = this.getPubAckCallbackSentAttrTelemetry(postAttributeMsg); + transportService.process(sessionInfo, postAttributeMsg, this.getPubAckCallbackSentAttrTelemetry(call)); + } else if (topicName.equals(LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC)) { + TransportProtos.PostTelemetryMsg postTelemetryMsg = adaptor.convertToPostTelemetry(msg); + TransportServiceCallback call = this.getPubAckCallbackSentAttrTelemetry(postTelemetryMsg); + transportService.process(sessionInfo, postTelemetryMsg, this.getPubAckCallbackSentAttrTelemetry(call)); + } + } catch (AdaptorException e) { + log.error("[{}] Failed to process publish msg [{}]", topicName, e); + log.info("[{}] Closing current session due to invalid publish", topicName); + } + } + + public JsonObject getTelemetryMsgObject(String logMsg) { + JsonObject telemetries = new JsonObject(); + telemetries.addProperty(LOG_LW2M_TELEMETRY, logMsg); + return telemetries; + } + + /** + * @return - sessionInfo after access connect client + */ + public TransportProtos.SessionInfoProto getValidateSessionInfo(TransportProtos.ValidateDeviceCredentialsResponseMsg msg, long mostSignificantBits, long leastSignificantBits) { + return TransportProtos.SessionInfoProto.newBuilder() + .setNodeId(this.getNodeId()) + .setSessionIdMSB(mostSignificantBits) + .setSessionIdLSB(leastSignificantBits) + .setDeviceIdMSB(msg.getDeviceInfo().getDeviceIdMSB()) + .setDeviceIdLSB(msg.getDeviceInfo().getDeviceIdLSB()) + .setTenantIdMSB(msg.getDeviceInfo().getTenantIdMSB()) + .setTenantIdLSB(msg.getDeviceInfo().getTenantIdLSB()) + .setDeviceName(msg.getDeviceInfo().getDeviceName()) + .setDeviceType(msg.getDeviceInfo().getDeviceType()) + .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileIdLSB()) + .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileIdMSB()) + .build(); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java new file mode 100644 index 0000000000..a62bdf2a49 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportHandler.java @@ -0,0 +1,347 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mObject; +import org.eclipse.leshan.core.node.LwM2mObjectInstance; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.server.californium.LeshanServerBuilder; +import org.nustaq.serialization.FSTConfiguration; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedList; +import java.util.Optional; + +@Slf4j +//@Component("LwM2MTransportHandler") +//@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true' )|| ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled}'=='true')") +public class LwM2mTransportHandler { + + // We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to + // send a Confirmable message to the time when an acknowledgement is no longer expected. + + public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; + public static final String ATTRIBUTE = "attribute"; + public static final String TELEMETRY = "telemetry"; + private static final String REQUEST = "/request"; + private static final String RESPONSE = "/response"; + private static final String ATTRIBUTES = "/" + ATTRIBUTE; + public static final String TELEMETRIES = "/" + TELEMETRY; + public static final String ATTRIBUTES_RESPONSE = ATTRIBUTES + RESPONSE; + public static final String ATTRIBUTES_REQUEST = ATTRIBUTES + REQUEST; + public static final String DEVICE_ATTRIBUTES_RESPONSE = ATTRIBUTES_RESPONSE + "/"; + public static final String DEVICE_ATTRIBUTES_REQUEST = ATTRIBUTES_REQUEST + "/"; + public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + ATTRIBUTES; + public static final String DEVICE_TELEMETRY_TOPIC = BASE_DEVICE_API_TOPIC + TELEMETRIES; + + public static final long DEFAULT_TIMEOUT = 2 * 60 * 1000L; // 2min in ms + public static final String OBSERVE_ATTRIBUTE_TELEMETRY = "observeAttr"; + public static final String CLIENT_LWM2M_SETTINGS = "clientLwM2mSettings"; + public static final String KEY_NAME = "keyName"; + public static final String OBSERVE = "observe"; + public static final String BOOTSTRAP = "bootstrap"; + public static final String SERVERS = "servers"; + public static final String LWM2M_SERVER = "lwm2mServer"; + public static final String BOOTSTRAP_SERVER = "bootstrapServer"; + + public static final String LOG_LW2M_TELEMETRY = "logLwm2m"; + public static final String LOG_LW2M_INFO = "info"; + public static final String LOG_LW2M_ERROR = "error"; + public static final String LOG_LW2M_WARN = "warn"; + + + public static final String CLIENT_NOT_AUTHORIZED = "Client not authorized"; + + public static final String GET_TYPE_OPER_READ = "read"; + public static final String GET_TYPE_OPER_DISCOVER = "discover"; + public static final String GET_TYPE_OPER_OBSERVE = "observe"; + public static final String POST_TYPE_OPER_OBSERVE_CANCEL = "observeCancel"; + public static final String POST_TYPE_OPER_EXECUTE = "execute"; + /** + * Replaces the Object Instance or the Resource(s) with the new value provided in the “Write” operation. (see + * section 5.3.3 of the LW M2M spec). + * if all resources are to be replaced + */ + public static final String POST_TYPE_OPER_WRITE_REPLACE = "replace"; + /** + * Adds or updates Resources provided in the new value and leaves other existing Resources unchanged. (see section + * 5.3.3 of the LW M2M spec). + * if this is a partial update request + */ + public static final String PUT_TYPE_OPER_WRITE_UPDATE = "update"; + public static final String PUT_TYPE_OPER_WRITE_ATTRIBUTES = "wright-attributes"; + + public static final String EVENT_AWAKE = "AWAKE"; + public static final String SERVICE_CHANNEL = "SERVICE"; + public static final String RESPONSE_CHANNEL = "RESP"; + +// @Autowired +// @Qualifier("LeshanServerCert") +// private LeshanServer lhServerCert; +// +// @Autowired +// @Qualifier("LeshanServerNoSecPskRpk") +// private LeshanServer lhServerNoSecPskRpk; + +// @Autowired +// @Qualifier("ServerListenerCert") +// private LwM2mServerListener serverListenerCert; +// +// @Autowired +// @Qualifier("ServerListenerNoSecPskRpk") +// private LwM2mServerListener serverListenerNoSecPskRpk; + + +// @PostConstruct +// public void init() { +// try { +// serverListenerCert.init(lhServerCert); +// this.lhServerCert.getRegistrationService().addListener(serverListenerCert.registrationListener); +// this.lhServerCert.getPresenceService().addListener(serverListenerCert.presenceListener); +// this.lhServerCert.getObservationService().addListener(serverListenerCert.observationListener); +// serverListenerNoSecPskRpk.init(lhServerNoSecPskRpk); +// this.lhServerNoSecPskRpk.getRegistrationService().addListener(serverListenerNoSecPskRpk.registrationListener); +// this.lhServerNoSecPskRpk.getPresenceService().addListener(serverListenerNoSecPskRpk.presenceListener); +// this.lhServerNoSecPskRpk.getObservationService().addListener(serverListenerNoSecPskRpk.observationListener); +// } catch (Exception e) { +// log.error("init [{}]", e.toString()); +// } +// } + + public static NetworkConfig getCoapConfig(Integer serverPortNoSec, Integer serverSecurePort) { + NetworkConfig coapConfig; + File configFile = new File(NetworkConfig.DEFAULT_FILE_NAME); + if (configFile.isFile()) { + coapConfig = new NetworkConfig(); + coapConfig.load(configFile); + } else { + coapConfig = LeshanServerBuilder.createDefaultNetworkConfig(); + coapConfig.store(configFile); + } + coapConfig.setString("COAP_PORT", Integer.toString(serverPortNoSec)); + coapConfig.setString("COAP_SECURE_PORT", Integer.toString(serverSecurePort)); + return coapConfig; + } + + public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath resourcePath) throws CodecException { + switch (type) { + case BOOLEAN: + case INTEGER: + case FLOAT: + return String.valueOf(valueOld).equals(String.valueOf(valueNew)); + case TIME: + return ((Date) valueOld).getTime() == ((Date) valueNew).getTime(); + case STRING: + case OBJLNK: + return valueOld.equals(valueNew); + case OPAQUE: + return Hex.decodeHex(((String) valueOld).toCharArray()).equals(Hex.decodeHex(((String) valueNew).toCharArray())); + default: + throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type); + } + } + + public static LwM2mNode getLvM2mNodeToObject(LwM2mNode content) { + if (content instanceof LwM2mObject) { + return (LwM2mObject) content; + } else if (content instanceof LwM2mObjectInstance) { + return (LwM2mObjectInstance) content; + } else if (content instanceof LwM2mSingleResource) { + return (LwM2mSingleResource) content; + } else if (content instanceof LwM2mMultipleResource) { + return (LwM2mMultipleResource) content; + } + return null; + } + + public static LwM2mClientProfile getNewProfileParameters(JsonObject profilesConfigData) { + LwM2mClientProfile lwM2MClientProfile = new LwM2mClientProfile(); + lwM2MClientProfile.setPostClientLwM2mSettings(profilesConfigData.get(CLIENT_LWM2M_SETTINGS).getAsJsonObject()); + lwM2MClientProfile.setPostKeyNameProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(KEY_NAME).getAsJsonObject()); + lwM2MClientProfile.setPostAttributeProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).getAsJsonArray()); + lwM2MClientProfile.setPostTelemetryProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).getAsJsonArray()); + lwM2MClientProfile.setPostObserveProfile(profilesConfigData.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).getAsJsonArray()); + return lwM2MClientProfile; + } + + /** + * @return deviceProfileBody with Observe&Attribute&Telemetry From Thingsboard + * Example: + * property: {"clientLwM2mSettings": { + * clientUpdateValueAfterConnect: false; + * } + * property: "observeAttr" + * {"keyName": { + * "/3/0/1": "modelNumber", + * "/3/0/0": "manufacturer", + * "/3/0/2": "serialNumber" + * }, + * "attribute":["/2/0/1","/3/0/9"], + * "telemetry":["/1/0/1","/2/0/1","/6/0/1"], + * "observe":["/2/0","/2/0/0","/4/0/2"]} + */ + public static LwM2mClientProfile getLwM2MClientProfileFromThingsboard(DeviceProfile deviceProfile) { + if (deviceProfile != null && ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) { + Object profile = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties(); + try { + ObjectMapper mapper = new ObjectMapper(); + String profileStr = mapper.writeValueAsString(profile); + JsonObject profileJson = (profileStr != null) ? validateJson(profileStr) : null; + return (getValidateCredentialsBodyFromThingsboard(profileJson)) ? LwM2mTransportHandler.getNewProfileParameters(profileJson) : null; + } catch (IOException e) { + log.error("", e); + } + } + return null; + } + + public static JsonObject getBootstrapParametersFromThingsboard(DeviceProfile deviceProfile) { + if (deviceProfile != null && ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties().size() > 0) { + Object bootstrap = ((Lwm2mDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration()).getProperties(); + try { + ObjectMapper mapper = new ObjectMapper(); + String bootstrapStr = mapper.writeValueAsString(bootstrap); + JsonObject objectMsg = (bootstrapStr != null) ? validateJson(bootstrapStr) : null; + return (getValidateBootstrapProfileFromThingsboard(objectMsg)) ? objectMsg.get(BOOTSTRAP).getAsJsonObject() : null; + } catch (IOException e) { + log.error("", e); + } + } + return null; + } + + public static boolean getClientOnlyObserveAfterConnect (LwM2mClientProfile profile) { + return profile.getPostClientLwM2mSettings().getAsJsonObject().has("clientOnlyObserveAfterConnect") && + profile.getPostClientLwM2mSettings().getAsJsonObject().get("clientOnlyObserveAfterConnect").getAsBoolean(); + } + + private static boolean getValidateCredentialsBodyFromThingsboard(JsonObject objectMsg) { + return (objectMsg != null && + !objectMsg.isJsonNull() && + objectMsg.has(CLIENT_LWM2M_SETTINGS) && + !objectMsg.get(CLIENT_LWM2M_SETTINGS).isJsonNull() && + objectMsg.get(CLIENT_LWM2M_SETTINGS).isJsonObject() && + objectMsg.has(OBSERVE_ATTRIBUTE_TELEMETRY) && + !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).isJsonNull() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).isJsonObject() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(KEY_NAME) && + !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(KEY_NAME).isJsonNull() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(KEY_NAME).isJsonObject() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(ATTRIBUTE) && + !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).isJsonNull() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(ATTRIBUTE).isJsonArray() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(TELEMETRY) && + !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).isJsonNull() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(TELEMETRY).isJsonArray() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().has(OBSERVE) && + !objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonNull() && + objectMsg.get(OBSERVE_ATTRIBUTE_TELEMETRY).getAsJsonObject().get(OBSERVE).isJsonArray()); + } + + private static boolean getValidateBootstrapProfileFromThingsboard(JsonObject objectMsg) { + return (objectMsg != null && + !objectMsg.isJsonNull() && + objectMsg.has(BOOTSTRAP) && + objectMsg.get(BOOTSTRAP).isJsonObject() && + !objectMsg.get(BOOTSTRAP).isJsonNull() && + objectMsg.get(BOOTSTRAP).getAsJsonObject().has(SERVERS) && + !objectMsg.get(BOOTSTRAP).getAsJsonObject().get(SERVERS).isJsonNull() && + objectMsg.get(BOOTSTRAP).getAsJsonObject().get(SERVERS).isJsonObject() && + objectMsg.get(BOOTSTRAP).getAsJsonObject().has(BOOTSTRAP_SERVER) && + !objectMsg.get(BOOTSTRAP).getAsJsonObject().get(BOOTSTRAP_SERVER).isJsonNull() && + objectMsg.get(BOOTSTRAP).getAsJsonObject().get(BOOTSTRAP_SERVER).isJsonObject() && + objectMsg.get(BOOTSTRAP).getAsJsonObject().has(LWM2M_SERVER) && + !objectMsg.get(BOOTSTRAP).getAsJsonObject().get(LWM2M_SERVER).isJsonNull() && + objectMsg.get(BOOTSTRAP).getAsJsonObject().get(LWM2M_SERVER).isJsonObject()); + } + + + public static JsonObject validateJson(String jsonStr) { + JsonObject object = null; + if (jsonStr != null && !jsonStr.isEmpty()) { + String jsonValidFlesh = jsonStr.replaceAll("\\\\", ""); + jsonValidFlesh = jsonValidFlesh.replaceAll("\n", ""); + jsonValidFlesh = jsonValidFlesh.replaceAll("\t", ""); + jsonValidFlesh = jsonValidFlesh.replaceAll(" ", ""); + String jsonValid = (jsonValidFlesh.charAt(0) == '"' && jsonValidFlesh.charAt(jsonValidFlesh.length() - 1) == '"') ? jsonValidFlesh.substring(1, jsonValidFlesh.length() - 1) : jsonValidFlesh; + try { + object = new JsonParser().parse(jsonValid).getAsJsonObject(); + } catch (JsonSyntaxException e) { + log.error("[{}] Fail validateJson [{}]", jsonStr, e.getMessage()); + } + } + return object; + } + + @SuppressWarnings("unchecked") + public static Optional decode(byte[] byteArray) { + try { + FSTConfiguration config = FSTConfiguration.createDefaultConfiguration(); + T msg = (T) config.asObject(byteArray); + return Optional.ofNullable(msg); + } catch (IllegalArgumentException e) { + log.error("Error during deserialization message, [{}]", e.getMessage()); + return Optional.empty(); + } + } + + public static String splitCamelCaseString(String s) { + LinkedList linkedListOut = new LinkedList<>(); + LinkedList linkedList = new LinkedList((Arrays.asList(s.split(" ")))); + linkedList.forEach(str -> { + String strOut = str.replaceAll("\\W", "").replaceAll("_", "").toUpperCase(); + if (strOut.length() > 1) linkedListOut.add(strOut.charAt(0) + strOut.substring(1).toLowerCase()); + else linkedListOut.add(strOut); + }); + linkedListOut.set(0, (linkedListOut.get(0).substring(0, 1).toLowerCase() + linkedListOut.get(0).substring(1))); + return StringUtils.join(linkedListOut, ""); + } + + public static TransportServiceCallback getAckCallback(LwM2mClient lwM2MClient, int requestId, String typeTopic) { + return new TransportServiceCallback() { + @Override + public void onSuccess(Void dummy) { + log.trace("[{}] [{}] - requestId [{}] - EndPoint , Access AckCallback", typeTopic, requestId, lwM2MClient.getEndpoint()); + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to publish msg", e.toString()); + } + }; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java new file mode 100644 index 0000000000..b05c79d6db --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java @@ -0,0 +1,330 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.leshan.core.attributes.Attribute; +import org.eclipse.leshan.core.attributes.AttributeSet; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mNode; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.ObjectLink; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.request.CancelObservationRequest; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.DiscoverRequest; +import org.eclipse.leshan.core.request.DownlinkRequest; +import org.eclipse.leshan.core.request.ExecuteRequest; +import org.eclipse.leshan.core.request.ObserveRequest; +import org.eclipse.leshan.core.request.ReadRequest; +import org.eclipse.leshan.core.request.WriteAttributesRequest; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.response.CancelObservationResponse; +import org.eclipse.leshan.core.response.DeleteResponse; +import org.eclipse.leshan.core.response.DiscoverResponse; +import org.eclipse.leshan.core.response.ExecuteResponse; +import org.eclipse.leshan.core.response.LwM2mResponse; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.response.ResponseCallback; +import org.eclipse.leshan.core.response.WriteAttributesResponse; +import org.eclipse.leshan.core.response.WriteResponse; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.core.util.NamedThreadFactory; +import org.eclipse.leshan.server.californium.LeshanServer; +import org.eclipse.leshan.server.registration.Registration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; +import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; + +import javax.annotation.PostConstruct; +import java.util.Date; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.eclipse.californium.core.coap.CoAP.ResponseCode.isSuccess; +import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_OBSERVE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_READ; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_ERROR; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_INFO; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_EXECUTE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_OBSERVE_CANCEL; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_WRITE_REPLACE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_ATTRIBUTES; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.PUT_TYPE_OPER_WRITE_UPDATE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.RESPONSE_CHANNEL; + +@Slf4j +@Service +@TbLwM2mTransportComponent +public class LwM2mTransportRequest { + private ExecutorService executorResponse; + + private LwM2mValueConverterImpl converter; + + private final LwM2mTransportContextServer context; + + private final LwM2mClientContext lwM2mClientContext; + + private final LeshanServer leshanServer; + + @Autowired + private LwM2mTransportServiceImpl serviceImpl; + + public LwM2mTransportRequest(LwM2mTransportContextServer context, LwM2mClientContext lwM2mClientContext, LeshanServer leshanServer) { + this.context = context; + this.lwM2mClientContext = lwM2mClientContext; + this.leshanServer = leshanServer; + } + + @PostConstruct + public void init() { + this.converter = LwM2mValueConverterImpl.getInstance(); + executorResponse = Executors.newFixedThreadPool(this.context.getLwM2MTransportConfigServer().getRequestPoolSize(), + new NamedThreadFactory(String.format("LwM2M %s channel response", RESPONSE_CHANNEL))); + } + + /** + * Device management and service enablement, including Read, Write, Execute, Discover, Create, Delete and Write-Attributes + * + * @param registration - + * @param target - + * @param typeOper - + * @param contentFormatParam - + * @param observation - + */ + public void sendAllRequest(Registration registration, String target, String typeOper, + String contentFormatParam, Observation observation, Object params, long timeoutInMs) { + LwM2mPath resultIds = new LwM2mPath(target); + if (registration != null && resultIds.getObjectId() >= 0) { + DownlinkRequest request = null; + ContentFormat contentFormat = contentFormatParam != null ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null; + ResourceModel resource = serviceImpl.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(registration, resultIds); + timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT; + switch (typeOper) { + case GET_TYPE_OPER_READ: + request = new ReadRequest(contentFormat, target); + break; + case GET_TYPE_OPER_DISCOVER: + request = new DiscoverRequest(target); + break; + case GET_TYPE_OPER_OBSERVE: + if (resultIds.isResource()) { + request = new ObserveRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId()); + } else if (resultIds.isObjectInstance()) { + request = new ObserveRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId()); + } else if (resultIds.getObjectId() >= 0) { + request = new ObserveRequest(resultIds.getObjectId()); + } + break; + case POST_TYPE_OPER_OBSERVE_CANCEL: + request = new CancelObservationRequest(observation); + break; + case POST_TYPE_OPER_EXECUTE: + if (params != null && resource != null && !resource.multiple) { + request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resource.type, ResourceModel.Type.STRING, resultIds)); + } else { + request = new ExecuteRequest(target); + } + break; + case POST_TYPE_OPER_WRITE_REPLACE: + // Request to write a String Single-Instance Resource using the TLV content format. + if (resource != null && contentFormat != null) { + if (contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { + request = this.getWriteRequestSingleResource(null, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resource.type, registration); + } + // Mode.REPLACE && Request to write a String Single-Instance Resource using the given content format (TEXT, TLV, JSON) + else if (!contentFormat.equals(ContentFormat.TLV) && !resource.multiple) { + request = this.getWriteRequestSingleResource(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resource.type, registration); + } + } + break; + case PUT_TYPE_OPER_WRITE_UPDATE: + if (resultIds.getResourceId() >= 0) { +// ResourceModel resourceModel = leshanServer.getModelProvider().getObjectModel(registration).getObjectModel(resultIds.getObjectId()).resources.get(resultIds.getResourceId()); +// ResourceModel.Type typeRes = resourceModel.type; + LwM2mNode node = LwM2mSingleResource.newStringResource(resultIds.getResourceId(), (String) this.converter.convertValue(params, resource.type, ResourceModel.Type.STRING, resultIds)); + request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, target, node); + } + break; + case PUT_TYPE_OPER_WRITE_ATTRIBUTES: + /** + * As example: + * a)Write-Attributes/3/0/9?pmin=1 means the Battery Level value will be notified + * to the Server with a minimum interval of 1sec; + * this value is set at theResource level. + * b)Write-Attributes/3/0/9?pmin means the Battery Level will be notified + * to the Server with a minimum value (pmin) given by the default one + * (resource 2 of Object Server ID=1), + * or with another value if this Attribute has been set at another level + * (Object or Object Instance: see section5.1.1). + * c)Write-Attributes/3/0?pmin=10 means that all Resources of Instance 0 of the Object ‘Device (ID:3)’ + * will be notified to the Server with a minimum interval of 10 sec; + * this value is set at the Object Instance level. + * d)Write-Attributes /3/0/9?gt=45&st=10 means the Battery Level will be notified to the Server + * when: + * a.old value is 20 and new value is 35 due to step condition + * b.old value is 45 and new value is 50 due to gt condition + * c.old value is 50 and new value is 40 due to both gt and step conditions + * d.old value is 35 and new value is 20 due to step conditione) + * Write-Attributes /3/0/9?lt=20>=85&st=10 means the Battery Level will be notified to the Server + * when: + * a.old value is 17 and new value is 24 due to lt condition + * b.old value is 75 and new value is 90 due to both gt and step conditions + * String uriQueries = "pmin=10&pmax=60"; + * AttributeSet attributes = AttributeSet.parse(uriQueries); + * WriteAttributesRequest request = new WriteAttributesRequest(target, attributes); + * Attribute gt = new Attribute(GREATER_THAN, Double.valueOf("45")); + * Attribute st = new Attribute(LESSER_THAN, Double.valueOf("10")); + * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); + * Attribute [] attrs = {gt, st}; + */ + Attribute pmin = new Attribute(MINIMUM_PERIOD, Integer.toUnsignedLong(Integer.parseInt("1"))); + Attribute[] attrs = {pmin}; + AttributeSet attrSet = new AttributeSet(attrs); + if (resultIds.isResource()) { + request = new WriteAttributesRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), attrSet); + } else if (resultIds.isObjectInstance()) { + request = new WriteAttributesRequest(resultIds.getObjectId(), resultIds.getObjectInstanceId(), attrSet); + } else if (resultIds.getObjectId() >= 0) { + request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet); + } + break; + default: + } + + if (request != null) { + this.sendRequest(registration, request, timeoutInMs); + } + } + } + + /** + * + * @param registration - + * @param request - + * @param timeoutInMs - + */ + + @SuppressWarnings("unchecked") + private void sendRequest(Registration registration, DownlinkRequest request, long timeoutInMs) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + leshanServer.send(registration, request, timeoutInMs, (ResponseCallback) response -> { + if (!lwM2MClient.isInit()) { + lwM2MClient.initValue(this.serviceImpl, request.getPath().toString()); + } + if (isSuccess(((Response) response.getCoapResponse()).getCode())) { + this.handleResponse(registration, request.getPath().toString(), response, request); + if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) { + String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client", + LOG_LW2M_INFO, ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString(), + ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue().toString()); + serviceImpl.sentLogsToThingsboard(msg, registration); + log.info("[{}] [{}] - [{}] [{}] Update SendRequest[{}]", registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString(), + ((LwM2mSingleResource) ((WriteRequest) request).getNode()).getValue()); + } + } else { + String msg = String.format("%s: sendRequest: CoapCode - %s Lwm2m code - %d name - %s Resource path - %s SendRequest to Client", LOG_LW2M_ERROR, + ((Response) response.getCoapResponse()).getCode(), response.getCode().getCode(), response.getCode().getName(), request.getPath().toString()); + serviceImpl.sentLogsToThingsboard(msg, registration); + log.error("[{}], [{}] - [{}] [{}] error SendRequest", registration.getEndpoint(), ((Response) response.getCoapResponse()).getCode(), response.getCode(), request.getPath().toString()); + } + }, e -> { + if (!lwM2MClient.isInit()) { + lwM2MClient.initValue(this.serviceImpl, request.getPath().toString()); + } + String msg = String.format("%s: sendRequest: Resource path - %s msg error - %s SendRequest to Client", + LOG_LW2M_ERROR, request.getPath().toString(), e.toString()); + serviceImpl.sentLogsToThingsboard(msg, registration); + log.error("[{}] - [{}] error SendRequest", request.getPath().toString(), e.toString()); + }); + + } + + private WriteRequest getWriteRequestSingleResource(ContentFormat contentFormat, Integer objectId, Integer instanceId, Integer resourceId, Object value, ResourceModel.Type type, Registration registration) { + try { + switch (type) { + case STRING: // String + return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, value.toString()) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, value.toString()); + case INTEGER: // Long + final long valueInt = Integer.toUnsignedLong(Integer.parseInt(value.toString())); + return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, valueInt) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, valueInt); + case OBJLNK: // ObjectLink + return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, ObjectLink.fromPath(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, ObjectLink.fromPath(value.toString())); + case BOOLEAN: // Boolean + return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Boolean.parseBoolean(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Boolean.parseBoolean(value.toString())); + case FLOAT: // Double + return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString())); + case TIME: // Date + Date date = new Date(Long.decode(value.toString())); + return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date); + case OPAQUE: // byte[] value, base64 + return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())); + default: + } + return null; + } catch (NumberFormatException e) { + String patn = "/" + objectId + "/" + instanceId + "/" + resourceId; + String msg = String.format(LOG_LW2M_ERROR + ": NumberFormatException: Resource path - %s type - %s value - %s msg error - %s SendRequest to Client", + patn, type, value, e.toString()); + serviceImpl.sentLogsToThingsboard(msg, registration); + log.error("Path: [{}] type: [{}] value: [{}] errorMsg: [{}]]", patn, type, value, e.toString()); + return null; + } + } + + private void handleResponse(Registration registration, final String path, LwM2mResponse response, DownlinkRequest request) { + executorResponse.submit(() -> { + try { + sendResponse(registration, path, response, request); + } catch (Exception e) { + log.error("[{}] endpoint [{}] path [{}] Exception Unable to after send response.", registration.getEndpoint(), path, e); + } + }); + } + + /** + * processing a response from a client + * @param registration - + * @param path - + * @param response - + */ + private void sendResponse(Registration registration, String path, LwM2mResponse response, DownlinkRequest request) { + if (response instanceof ReadResponse) { + serviceImpl.onObservationResponse(registration, path, (ReadResponse) response); + } else if (response instanceof CancelObservationResponse) { + log.info("[{}] Path [{}] CancelObservationResponse 3_Send", path, response); + } else if (response instanceof DeleteResponse) { + log.info("[{}] Path [{}] DeleteResponse 5_Send", path, response); + } else if (response instanceof DiscoverResponse) { + log.info("[{}] Path [{}] DiscoverResponse 6_Send", path, response); + } else if (response instanceof ExecuteResponse) { + log.info("[{}] Path [{}] ExecuteResponse 7_Send", path, response); + } else if (response instanceof WriteAttributesResponse) { + log.info("[{}] Path [{}] WriteAttributesResponse 8_Send", path, response); + } else if (response instanceof WriteResponse) { + log.info("[{}] Path [{}] WriteAttributesResponse 9_Send", path, response); + serviceImpl.onWriteResponseOk(registration, path, (WriteRequest) request); + } + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerConfiguration.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerConfiguration.java new file mode 100644 index 0000000000..ace2fb4896 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerConfiguration.java @@ -0,0 +1,293 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.server.californium.LeshanServer; +import org.eclipse.leshan.server.californium.LeshanServerBuilder; +import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.eclipse.leshan.server.model.LwM2mModelProvider; +import org.eclipse.leshan.server.model.VersionedModelProvider; +import org.eclipse.leshan.server.security.DefaultAuthorizer; +import org.eclipse.leshan.server.security.EditableSecurityStore; +import org.eclipse.leshan.server.security.SecurityChecker; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.KeyFactory; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; + +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256; +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8; +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256; +import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_PSK_WITH_AES_128_CCM_8; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.getCoapConfig; + +@Slf4j +@Component("LwM2MTransportServerConfiguration") +@TbLwM2mTransportComponent +public class LwM2mTransportServerConfiguration { + private PublicKey publicKey; + private PrivateKey privateKey; + private boolean pskMode = false; + + @Autowired + private LwM2mTransportContextServer context; + + @Autowired + private CaliforniumRegistrationStore registrationStore; + + @Autowired + private EditableSecurityStore securityStore; + + @Bean + public LeshanServer getLeshanServer() { + log.info("Starting LwM2M transport Server... PostConstruct"); + return this.getLhServer(this.context.getLwM2MTransportConfigServer().getServerPortNoSec(), this.context.getLwM2MTransportConfigServer().getServerPortSecurity()); + } + + private LeshanServer getLhServer(Integer serverPortNoSec, Integer serverSecurePort) { + LeshanServerBuilder builder = new LeshanServerBuilder(); + builder.setLocalAddress(this.context.getLwM2MTransportConfigServer().getServerHost(), serverPortNoSec); + builder.setLocalSecureAddress(this.context.getLwM2MTransportConfigServer().getServerHostSecurity(), serverSecurePort); + builder.setDecoder(new DefaultLwM2mNodeDecoder()); + /** Use a magic converter to support bad type send by the UI. */ + builder.setEncoder(new DefaultLwM2mNodeEncoder(LwM2mValueConverterImpl.getInstance())); + + /** Create CoAP Config */ + builder.setCoapConfig(getCoapConfig(serverPortNoSec, serverSecurePort)); + + /** Define model provider (Create Models )*/ + LwM2mModelProvider modelProvider = new VersionedModelProvider(this.context.getLwM2MTransportConfigServer().getModelsValue()); + builder.setObjectModelProvider(modelProvider); + + /** Create credentials */ + this.setServerWithCredentials(builder); + + /** Set securityStore with new registrationStore */ + builder.setSecurityStore(securityStore); + builder.setRegistrationStore(registrationStore); + + + /** Create DTLS Config */ + DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); + dtlsConfig.setRecommendedSupportedGroupsOnly(this.context.getLwM2MTransportConfigServer().isRecommendedSupportedGroups()); + dtlsConfig.setRecommendedCipherSuitesOnly(this.context.getLwM2MTransportConfigServer().isRecommendedCiphers()); + if (this.pskMode) { + dtlsConfig.setSupportedCipherSuites( + TLS_PSK_WITH_AES_128_CCM_8, + TLS_PSK_WITH_AES_128_CBC_SHA256); + } else { + dtlsConfig.setSupportedCipherSuites( + TLS_PSK_WITH_AES_128_CCM_8, + TLS_PSK_WITH_AES_128_CBC_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); + } + + /** Set DTLS Config */ + builder.setDtlsConfig(dtlsConfig); + + /** Create LWM2M server */ + return builder.build(); + } + + private void setServerWithCredentials(LeshanServerBuilder builder) { + try { + if (this.context.getLwM2MTransportConfigServer().getKeyStoreValue() != null) { + if (this.setBuilderX509(builder)) { + X509Certificate rootCAX509Cert = (X509Certificate) this.context.getLwM2MTransportConfigServer().getKeyStoreValue().getCertificate(this.context.getLwM2MTransportConfigServer().getRootAlias()); + if (rootCAX509Cert != null) { + X509Certificate[] trustedCertificates = new X509Certificate[1]; + trustedCertificates[0] = rootCAX509Cert; + builder.setTrustedCertificates(trustedCertificates); + } else { + /** by default trust all */ + builder.setTrustedCertificates(new X509Certificate[0]); + } + /** Set securityStore with registrationStore*/ + builder.setAuthorizer(new DefaultAuthorizer(securityStore, new SecurityChecker() { + @Override + protected boolean matchX509Identity(String endpoint, String receivedX509CommonName, + String expectedX509CommonName) { + return endpoint.startsWith(expectedX509CommonName); + } + })); + } + } else if (this.setServerRPK(builder)) { + this.infoPramsUri("RPK"); + this.infoParamsServerKey(this.publicKey, this.privateKey); + } else { + /** by default trust all */ + builder.setTrustedCertificates(new X509Certificate[0]); + log.info("Unable to load X509 files for LWM2MServer"); + this.pskMode = true; + this.infoPramsUri("PSK"); + } + } catch (KeyStoreException ex) { + log.error("[{}] Unable to load X509 files server", ex.getMessage()); + } + } + + private boolean setBuilderX509(LeshanServerBuilder builder) { + /** + * For deb => KeyStorePathFile == yml or commandline: KEY_STORE_PATH_FILE + * For idea => KeyStorePathResource == common/transport/lwm2m/src/main/resources/credentials: in LwM2MTransportContextServer: credentials/serverKeyStore.jks + */ + try { + X509Certificate serverCertificate = (X509Certificate) this.context.getLwM2MTransportConfigServer().getKeyStoreValue().getCertificate(this.context.getLwM2MTransportConfigServer().getServerAlias()); + PrivateKey privateKey = (PrivateKey) this.context.getLwM2MTransportConfigServer().getKeyStoreValue().getKey(this.context.getLwM2MTransportConfigServer().getServerAlias(), this.context.getLwM2MTransportConfigServer().getKeyStorePasswordServer() == null ? null : this.context.getLwM2MTransportConfigServer().getKeyStorePasswordServer().toCharArray()); + PublicKey publicKey = serverCertificate.getPublicKey(); + if (serverCertificate != null && + privateKey != null && privateKey.getEncoded().length > 0 && + publicKey != null && publicKey.getEncoded().length > 0) { + builder.setPublicKey(serverCertificate.getPublicKey()); + builder.setPrivateKey(privateKey); + builder.setCertificateChain(new X509Certificate[]{serverCertificate}); + this.infoParamsServerX509(serverCertificate, publicKey, privateKey); + return true; + } else { + return false; + } + } catch (Exception ex) { + log.error("[{}] Unable to load KeyStore files server", ex.getMessage()); + return false; + } + } + + private void infoParamsServerX509(X509Certificate certificate, PublicKey publicKey, PrivateKey privateKey) { + try { + infoPramsUri("X509"); + log.info("\n- X509 Certificate (Hex): [{}]", + Hex.encodeHexString(certificate.getEncoded())); + this.infoParamsServerKey(publicKey, privateKey); + } catch (CertificateEncodingException e) { + log.error("", e); + } + } + + private void infoPramsUri(String mode) { + log.info("Server uses [{}]: serverNoSecureURI : [{}], serverSecureURI : [{}]", + mode, + this.context.getLwM2MTransportConfigServer().getServerHost() + ":" + this.context.getLwM2MTransportConfigServer().getServerPortNoSec(), + this.context.getLwM2MTransportConfigServer().getServerHostSecurity() + ":" + this.context.getLwM2MTransportConfigServer().getServerPortSecurity()); + } + + private boolean setServerRPK(LeshanServerBuilder builder) { + try { + this.generateKeyForRPK(); + if (this.publicKey != null && this.publicKey.getEncoded().length > 0 && + this.privateKey != null && this.privateKey.getEncoded().length > 0) { + builder.setPublicKey(this.publicKey); + builder.setPrivateKey(this.privateKey); + return true; + } + } catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e) { + log.error("Fail create Server with RPK", e); + } + return false; + } + + + /** + * From yml: server + * public_x: "${LWM2M_SERVER_PUBLIC_X:405354ea8893471d9296afbc8b020a5c6201b0bb25812a53b849d4480fa5f069}" + * public_y: "${LWM2M_SERVER_PUBLIC_Y:30c9237e946a3a1692c1cafaa01a238a077f632c99371348337512363f28212b}" + * private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:274671fe40ce937b8a6352cf0a418e8a39e4bf0bb9bf74c910db953c20c73802}" + */ + private void generateKeyForRPK() throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException { + /** Get Elliptic Curve Parameter spec for secp256r1 */ + AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); + algoParameters.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); + if (this.context.getLwM2MTransportConfigServer().getServerPublicX() != null && + !this.context.getLwM2MTransportConfigServer().getServerPublicX().isEmpty() && + this.context.getLwM2MTransportConfigServer().getServerPublicY() != null && + !this.context.getLwM2MTransportConfigServer().getServerPublicY().isEmpty()) { + /** Get point values */ + byte[] publicX = Hex.decodeHex(this.context.getLwM2MTransportConfigServer().getServerPublicX().toCharArray()); + byte[] publicY = Hex.decodeHex(this.context.getLwM2MTransportConfigServer().getServerPublicY().toCharArray()); + /** Create key specs */ + KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), + parameterSpec); + /** Get keys */ + this.publicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); + } + if (this.context.getLwM2MTransportConfigServer().getServerPrivateEncoded() != null && + !this.context.getLwM2MTransportConfigServer().getServerPrivateEncoded().isEmpty()) { + /** Get private key */ + byte[] privateS = Hex.decodeHex(this.context.getLwM2MTransportConfigServer().getServerPrivateEncoded().toCharArray()); + try { + this.privateKey = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(privateS)); + } catch (InvalidKeySpecException ignore2) { + log.error("Invalid Server rpk.PrivateKey.getEncoded () [{}}]. PrivateKey has no EC algorithm", this.context.getLwM2MTransportConfigServer().getServerPrivateEncoded()); + } + } + } + + private void infoParamsServerKey(PublicKey publicKey, PrivateKey privateKey) { + /** Get x coordinate */ + byte[] x = ((ECPublicKey) publicKey).getW().getAffineX().toByteArray(); + if (x[0] == 0) + x = Arrays.copyOfRange(x, 1, x.length); + + /** Get Y coordinate */ + byte[] y = ((ECPublicKey) publicKey).getW().getAffineY().toByteArray(); + if (y[0] == 0) + y = Arrays.copyOfRange(y, 1, y.length); + + /** Get Curves params */ + String params = ((ECPublicKey) publicKey).getParams().toString(); + String privHex = Hex.encodeHexString(privateKey.getEncoded()); + log.info(" \n- Public Key (Hex): [{}] \n" + + "- Private Key (Hex): [{}], \n" + + "public_x: \"${LWM2M_SERVER_PUBLIC_X:{}}\" \n" + + "public_y: \"${LWM2M_SERVER_PUBLIC_Y:{}}\" \n" + + "private_encoded: \"${LWM2M_SERVER_PRIVATE_ENCODED:{}}\" \n" + + "- Elliptic Curve parameters : [{}]", + Hex.encodeHexString(publicKey.getEncoded()), + privHex, + Hex.encodeHexString(x), + Hex.encodeHexString(y), + privHex, + params); + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerInitializer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerInitializer.java new file mode 100644 index 0000000000..4fad90910a --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServerInitializer.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.server.californium.LeshanServer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.secure.LWM2MGenerationPSkRPkECC; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +@Slf4j +@Component("LwM2MTransportServerInitializer") +@TbLwM2mTransportComponent +public class LwM2mTransportServerInitializer { + + @Autowired + private LwM2mTransportServiceImpl service; + + @Autowired + private LeshanServer leshanServer; + + @Autowired + private LwM2mTransportContextServer context; + + @PostConstruct + public void init() { + if (this.context.getLwM2MTransportConfigServer().getEnableGenNewKeyPskRpk()) { + new LWM2MGenerationPSkRPkECC(); + } + this.startLhServer(); + } + + private void startLhServer() { + this.leshanServer.start(); + LwM2mServerListener lhServerCertListener = new LwM2mServerListener(service); + this.leshanServer.getRegistrationService().addListener(lhServerCertListener.registrationListener); + this.leshanServer.getPresenceService().addListener(lhServerCertListener.presenceListener); + this.leshanServer.getObservationService().addListener(lhServerCertListener.observationListener); + } + + @PreDestroy + public void shutdown() { + log.info("Stopping LwM2M transport Server!"); + leshanServer.destroy(); + log.info("LwM2M transport Server stopped!"); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java new file mode 100644 index 0000000000..8d1aff37b5 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportService.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.server.registration.Registration; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.Collection; +import java.util.Optional; + +public interface LwM2mTransportService { + + void onRegistered(Registration registration, Collection previousObsersations); + + void updatedReg(Registration registration); + + void unReg(Registration registration, Collection observations); + + void onSleepingDev(Registration registration); + + void setCancelObservations(Registration registration); + + void setCancelObservationRecourse(Registration registration, String path); + + void onObservationResponse(Registration registration, String path, ReadResponse response); + + void onAttributeUpdate(TransportProtos.AttributeUpdateNotificationMsg msg, TransportProtos.SessionInfoProto sessionInfo); + + void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile); + + void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt); + + void doTrigger(Registration registration, String path); + + void doDisconnect(TransportProtos.SessionInfoProto sessionInfo); + + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java new file mode 100644 index 0000000000..548d750a3f --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java @@ -0,0 +1,1121 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mObject; +import org.eclipse.leshan.core.node.LwM2mObjectInstance; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.core.request.WriteRequest; +import org.eclipse.leshan.core.response.ReadResponse; +import org.eclipse.leshan.core.util.NamedThreadFactory; +import org.eclipse.leshan.server.californium.LeshanServer; +import org.eclipse.leshan.server.registration.Registration; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.service.DefaultTransportService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; +import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue; +import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters; +import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.transport.util.JsonUtils.getJsonObject; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.CLIENT_NOT_AUTHORIZED; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_ATTRIBUTES_TOPIC; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEVICE_TELEMETRY_TOPIC; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_OBSERVE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_READ; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_ERROR; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_INFO; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.LOG_LW2M_TELEMETRY; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_EXECUTE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.POST_TYPE_OPER_WRITE_REPLACE; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.SERVICE_CHANNEL; +import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.getAckCallback; + +@Slf4j +@Service +@TbLwM2mTransportComponent +public class LwM2mTransportServiceImpl implements LwM2mTransportService { + + private ExecutorService executorRegistered; + private ExecutorService executorUpdateRegistered; + private ExecutorService executorUnRegistered; + private LwM2mValueConverterImpl converter; + protected final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + protected final Lock writeLock = readWriteLock.writeLock(); + + private final TransportService transportService; + + public final LwM2mTransportContextServer lwM2mTransportContextServer; + + private final LwM2mClientContext lwM2mClientContext; + + private final LeshanServer leshanServer; + + private final LwM2mTransportRequest lwM2mTransportRequest; + + public LwM2mTransportServiceImpl(TransportService transportService, LwM2mTransportContextServer lwM2mTransportContextServer, LwM2mClientContext lwM2mClientContext, LeshanServer leshanServer, @Lazy LwM2mTransportRequest lwM2mTransportRequest) { + this.transportService = transportService; + this.lwM2mTransportContextServer = lwM2mTransportContextServer; + this.lwM2mClientContext = lwM2mClientContext; + this.leshanServer = leshanServer; + this.lwM2mTransportRequest = lwM2mTransportRequest; + } + + @PostConstruct + public void init() { + this.lwM2mTransportContextServer.getScheduler().scheduleAtFixedRate(this::checkInactivityAndReportActivity, new Random().nextInt((int) lwM2mTransportContextServer.getLwM2MTransportConfigServer().getSessionReportTimeout()), lwM2mTransportContextServer.getLwM2MTransportConfigServer().getSessionReportTimeout(), TimeUnit.MILLISECONDS); + this.executorRegistered = Executors.newFixedThreadPool(this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getRegisteredPoolSize(), + new NamedThreadFactory(String.format("LwM2M %s channel registered", SERVICE_CHANNEL))); + this.executorUpdateRegistered = Executors.newFixedThreadPool(this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getUpdateRegisteredPoolSize(), + new NamedThreadFactory(String.format("LwM2M %s channel update registered", SERVICE_CHANNEL))); + this.executorUnRegistered = Executors.newFixedThreadPool(this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getUnRegisteredPoolSize(), + new NamedThreadFactory(String.format("LwM2M %s channel un registered", SERVICE_CHANNEL))); + this.converter = LwM2mValueConverterImpl.getInstance(); + } + + /** + * Start registration device + * Create session: Map, LwM2MClient> + * 1. replaceNewRegistration -> (solving the problem of incorrect termination of the previous session with this endpoint) + * 1.1 When we initialize the registration, we register the session by endpoint. + * 1.2 If the server has incomplete requests (canceling the registration of the previous session), + * delete the previous session only by the previous registration.getId + * 1.2 Add Model (Entity) for client (from registration & observe) by registration.getId + * 1.2 Remove from sessions Model by enpPoint + * Next -> Create new LwM2MClient for current session -> setModelClient... + * + * @param registration - Registration LwM2M Client + * @param previousObsersations - may be null + */ + public void onRegistered(Registration registration, Collection previousObsersations) { + executorRegistered.submit(() -> { + try { + log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId()); + LwM2mClient lwM2MClient = this.lwM2mClientContext.updateInSessionsLwM2MClient(registration); + if (lwM2MClient != null) { + SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); + if (sessionInfo != null) { + lwM2MClient.setDeviceId(new UUID(sessionInfo.getDeviceIdMSB(), sessionInfo.getDeviceIdLSB())); + lwM2MClient.setProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); + lwM2MClient.setDeviceName(sessionInfo.getDeviceName()); + lwM2MClient.setDeviceProfileName(sessionInfo.getDeviceType()); + transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); + transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), null); + transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); + this.sentLogsToThingsboard(LOG_LW2M_INFO + ": Client create after Registration", registration); + this.initLwM2mFromClientValue(registration, lwM2MClient); + } else { + log.error("Client: [{}] onRegistered [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); + } + } else { + log.error("Client: [{}] onRegistered [{}] name [{}] lwM2MClient ", registration.getId(), registration.getEndpoint(), null); + } + } catch (Throwable t) { + log.error("[{}] endpoint [{}] error Unable registration.", registration.getEndpoint(), t); + } + }); + } + + /** + * if sessionInfo removed from sessions, then new registerAsyncSession + * + * @param registration - Registration LwM2M Client + */ + public void updatedReg(Registration registration) { + executorUpdateRegistered.submit(() -> { + try { + SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); + if (sessionInfo != null) { + this.checkInactivity(sessionInfo); + log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); + } else { + log.error("Client: [{}] updatedReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); + } + } catch (Throwable t) { + log.error("[{}] endpoint [{}] error Unable update registration.", registration.getEndpoint(), t); + } + }); + } + + /** + * @param registration - Registration LwM2M Client + * @param observations - All paths observations before unReg + * !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect + */ + public void unReg(Registration registration, Collection observations) { + executorUnRegistered.submit(() -> { + try { + this.setCancelObservations(registration); + this.sentLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration); + this.closeClientSession(registration); + } catch (Throwable t) { + log.error("[{}] endpoint [{}] error Unable un registration.", registration.getEndpoint(), t); + } + }); + } + + private void closeClientSession(Registration registration) { + SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); + if (sessionInfo != null) { + transportService.deregisterSession(sessionInfo); + this.doCloseSession(sessionInfo); + lwM2mClientContext.delRemoveSessionAndListener(registration.getId()); + if (lwM2mClientContext.getProfiles().size() > 0) { + this.syncSessionsAndProfiles(); + } + log.info("Client close session: [{}] unReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); + } else { + log.error("Client close session: [{}] unReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); + } + } + + public void onSleepingDev(Registration registration) { + log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint()); + //TODO: associate endpointId with device information. + } + + @Override + public void setCancelObservations(Registration registration) { + if (registration != null) { + Set observations = leshanServer.getObservationService().getObservations(registration); + observations.forEach(observation -> this.setCancelObservationRecourse(registration, observation.getPath().toString())); + } + } + + /** + * lwM2MTransportRequest.sendAllRequest(lwServer, registration, path, POST_TYPE_OPER_OBSERVE_CANCEL, null, null, null, null, context.getTimeout()); + * At server side this will not remove the observation from the observation store, to do it you need to use + * {@code ObservationService#cancelObservation()} + */ + @Override + public void setCancelObservationRecourse(Registration registration, String path) { + leshanServer.getObservationService().cancelObservations(registration, path); + } + + /** + * Sending observe value to thingsboard from ObservationListener.onResponse: object, instance, SingleResource or MultipleResource + * + * @param registration - Registration LwM2M Client + * @param path - observe + * @param response - observe + */ + @Override + public void onObservationResponse(Registration registration, String path, ReadResponse response) { + if (response.getContent() != null) { + if (response.getContent() instanceof LwM2mObject) { + LwM2mObject lwM2mObject = (LwM2mObject) response.getContent(); + this.updateObjectResourceValue(registration, lwM2mObject, path); + } else if (response.getContent() instanceof LwM2mObjectInstance) { + LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) response.getContent(); + this.updateObjectInstanceResourceValue(registration, lwM2mObjectInstance, path); + } else if (response.getContent() instanceof LwM2mResource) { + LwM2mResource lwM2mResource = (LwM2mResource) response.getContent(); + this.updateResourcesValue(registration, lwM2mResource, path); + } + } + } + + /** + * Update - sent request in change value resources in Client + * Path to resources from profile equal keyName or from ModelObject equal name + * Only for resources: isWritable && isPresent as attribute in profile -> LwM2MClientProfile (format: CamelCase) + * Delete - nothing * + * + * @param msg - + */ + @Override + public void onAttributeUpdate(AttributeUpdateNotificationMsg msg, TransportProtos.SessionInfoProto sessionInfo) { + if (msg.getSharedUpdatedCount() > 0) { + JsonElement el = JsonConverter.toJson(msg); + el.getAsJsonObject().entrySet().forEach(de -> { + String path = this.getPathAttributeUpdate(sessionInfo, de.getKey()); + String value = de.getValue().getAsString(); + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClient(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())); + LwM2mClientProfile clientProfile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); + if (path != null && !path.isEmpty() && (this.validatePathInAttrProfile(clientProfile, path) || this.validatePathInTelemetryProfile(clientProfile, path))) { + ResourceModel resourceModel = lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(lwM2MClient.getRegistration(), new LwM2mPath(path)); + if (resourceModel != null && resourceModel.operations.isWritable()) { + lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), path, POST_TYPE_OPER_WRITE_REPLACE, + ContentFormat.TLV.getName(), null, value, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + } else { + log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", path, value); + String logMsg = String.format("%s: attributeUpdate: Resource path - %s value - %s is not Writable and cannot be updated", + LOG_LW2M_ERROR, path, value); + this.sentLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); + } + } else { + log.error("Attribute name - [{}] value - [{}] is not present as attribute in profile and cannot be updated", de.getKey(), value); + String logMsg = String.format("%s: attributeUpdate: attribute name - %s value - %s is not present as attribute in profile and cannot be updated", + LOG_LW2M_ERROR, de.getKey(), value); + this.sentLogsToThingsboard(logMsg, lwM2MClient.getRegistration()); + } + }); + } else if (msg.getSharedDeletedCount() > 0) { + log.info("[{}] delete [{}] onAttributeUpdate", msg.getSharedDeletedList(), sessionInfo); + } + } + + /** + * @param sessionInfo - + * @param deviceProfile - + */ + @Override + public void onDeviceProfileUpdate(SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { + Set registrationIds = lwM2mClientContext.getLwM2mClients().entrySet() + .stream() + .filter(e -> e.getValue().getProfileId().equals(deviceProfile.getUuidId())) + .map(Map.Entry::getKey).sorted().collect(Collectors.toCollection(LinkedHashSet::new)); + if (registrationIds.size() > 0) { + this.onDeviceUpdateChangeProfile(registrationIds, deviceProfile); + } + } + + /** + * @param sessionInfo - + * @param device - + * @param deviceProfileOpt - + */ + @Override + public void onDeviceUpdate(SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { + Optional registrationIdOpt = lwM2mClientContext.getLwM2mClients().entrySet().stream() + .filter(e -> device.getUuidId().equals(e.getValue().getDeviceId())) + .map(Map.Entry::getKey) + .findFirst(); + registrationIdOpt.ifPresent(registrationId -> this.onDeviceUpdateLwM2MClient(registrationId, device, deviceProfileOpt)); + } + + /** + * Trigger Server path = "/1/0/8" + * + * Trigger bootStrap path = "/1/0/9" - have to implemented on client + */ + @Override + public void doTrigger(Registration registration, String path) { + lwM2mTransportRequest.sendAllRequest(registration, path, POST_TYPE_OPER_EXECUTE, + ContentFormat.TLV.getName(), null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + } + + /** + * Deregister session in transport + * + * @param sessionInfo - lwm2m client + */ + @Override + public void doDisconnect(SessionInfoProto sessionInfo) { + transportService.process(sessionInfo, DefaultTransportService.getSessionEventMsg(SessionEvent.CLOSED), null); + transportService.deregisterSession(sessionInfo); + } + + /** + * Session device in thingsboard is closed + * + * @param sessionInfo - lwm2m client + */ + private void doCloseSession(SessionInfoProto sessionInfo) { + TransportProtos.SessionEvent event = SessionEvent.CLOSED; + TransportProtos.SessionEventMsg msg = TransportProtos.SessionEventMsg.newBuilder() + .setSessionType(TransportProtos.SessionType.ASYNC) + .setEvent(event).build(); + transportService.process(sessionInfo, msg, null); + } + + /** + * Those methods are called by the protocol stage thread pool, this means that execution MUST be done in a short delay, + * * if you need to do long time processing use a dedicated thread pool. + * + * @param registration - + */ + protected void onAwakeDev(Registration registration) { + log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint()); + //TODO: associate endpointId with device information. + } + + /** + * This method is used to sync with sessions + * Removes a profile if not used in sessions + */ + private void syncSessionsAndProfiles() { + Map profilesClone = lwM2mClientContext.getProfiles().entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + profilesClone.forEach((k, v) -> { + String registrationId = lwM2mClientContext.getLwM2mClients().entrySet() + .stream() + .filter(e -> e.getValue().getProfileId().equals(k)) + .findFirst() + .map(Map.Entry::getKey) // return the key of the matching entry if found + .orElse(""); + if (registrationId.isEmpty()) { + lwM2mClientContext.getProfiles().remove(k); + } + }); + } + + /** + * @param msg - text msg + * @param registration - Id of Registration LwM2M Client + */ + public void sentLogsToThingsboard(String msg, Registration registration) { + if (msg != null) { + JsonObject telemetries = new JsonObject(); + telemetries.addProperty(LOG_LW2M_TELEMETRY, msg); + this.updateParametersOnThingsboard(telemetries, DEVICE_TELEMETRY_TOPIC, registration); + } + } + + + /** + * // !!! Ok + * Prepare Sent to Thigsboard callback - Attribute or Telemetry + * + * @param msg - JsonArray: [{name: value}] + * @param topicName - Api Attribute or Telemetry + * @param registration - Id of Registration LwM2M Client + */ + public void updateParametersOnThingsboard(JsonElement msg, String topicName, Registration registration) { + SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration); + if (sessionInfo != null) { + lwM2mTransportContextServer.sentParametersOnThingsboard(msg, topicName, sessionInfo); + } else { + log.error("Client: [{}] updateParametersOnThingsboard [{}] sessionInfo ", registration, null); + } + } + + /** + * #1 clientOnlyObserveAfterConnect == true + * - Only Observe Request to the client marked as observe from the profile configuration. + * #2. clientOnlyObserveAfterConnect == false + * После регистрации отправляю запрос на read всех ресурсов, которые после регистрации есть у клиента, + * а затем запрос на observe (edited) + * - Read Request to the client after registration to read all resource values for all objects + * - then Observe Request to the client marked as observe from the profile configuration. + * + * @param registration - Registration LwM2M Client + * @param lwM2MClient - object with All parameters off client + */ + private void initLwM2mFromClientValue(Registration registration, LwM2mClient lwM2MClient) { + LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); + Set clientObjects = this.getAllOjectsInClient(registration); + if (clientObjects != null && !LwM2mTransportHandler.getClientOnlyObserveAfterConnect(lwM2MClientProfile)) { + // #2 + lwM2MClient.getPendingRequests().addAll(clientObjects); + clientObjects.forEach(path -> lwM2mTransportRequest.sendAllRequest(registration, path, GET_TYPE_OPER_READ, ContentFormat.TLV.getName(), + null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + } + // #1 + this.initReadAttrTelemetryObserveToClient(registration, lwM2MClient, GET_TYPE_OPER_OBSERVE); + } + + /** + * @param registration - + * @param lwM2mObject - + * @param path - + */ + private void updateObjectResourceValue(Registration registration, LwM2mObject lwM2mObject, String path) { + LwM2mPath pathIds = new LwM2mPath(path); + lwM2mObject.getInstances().forEach((instanceId, instance) -> { + String pathInstance = pathIds.toString() + "/" + instanceId; + this.updateObjectInstanceResourceValue(registration, instance, pathInstance); + }); + } + + /** + * @param registration - + * @param lwM2mObjectInstance - + * @param path - + */ + private void updateObjectInstanceResourceValue(Registration registration, LwM2mObjectInstance lwM2mObjectInstance, String path) { + LwM2mPath pathIds = new LwM2mPath(path); + lwM2mObjectInstance.getResources().forEach((resourceId, resource) -> { + String pathRez = pathIds.toString() + "/" + resourceId; + this.updateResourcesValue(registration, resource, pathRez); + }); + } + + /** + * Sending observe value of resources to thingsboard + * #1 Return old Value Resource from LwM2MClient + * #2 Update new Resources (replace old Resource Value on new Resource Value) + * + * @param registration - Registration LwM2M Client + * @param lwM2mResource - LwM2mSingleResource response.getContent() + * @param path - resource + */ + private void updateResourcesValue(Registration registration, LwM2mResource lwM2mResource, String path) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + lwM2MClient.updateResourceValue(path, lwM2mResource); + Set paths = new HashSet<>(); + paths.add(path); + this.updateAttrTelemetry(registration, paths); + } + + /** + * Sent Attribute and Telemetry to Thingsboard + * #1 - get AttrName/TelemetryName with value from LwM2MClient: + * -- resourceId == path from LwM2MClientProfile.postAttributeProfile/postTelemetryProfile/postObserveProfile + * -- AttrName/TelemetryName == resourceName from ModelObject.objectModel, value from ModelObject.instance.resource(resourceId) + * #2 - set Attribute/Telemetry + * + * @param registration - Registration LwM2M Client + */ + private void updateAttrTelemetry(Registration registration, Set paths) { + JsonObject attributes = new JsonObject(); + JsonObject telemetries = new JsonObject(); + try { + writeLock.lock(); + this.getParametersFromProfile(attributes, telemetries, registration, paths); + } catch (Exception e) { + log.error("UpdateAttrTelemetry", e); + } finally { + writeLock.unlock(); + } + if (attributes.getAsJsonObject().entrySet().size() > 0) + this.updateParametersOnThingsboard(attributes, DEVICE_ATTRIBUTES_TOPIC, registration); + if (telemetries.getAsJsonObject().entrySet().size() > 0) + this.updateParametersOnThingsboard(telemetries, DEVICE_TELEMETRY_TOPIC, registration); + } + + /** + * @param clientProfile - + * @param path - + * @return true if path isPresent in postAttributeProfile + */ + private boolean validatePathInAttrProfile(LwM2mClientProfile clientProfile, String path) { + try { + List attributesSet = new Gson().fromJson(clientProfile.getPostAttributeProfile(), new TypeToken<>() { + }.getType()); + return attributesSet.stream().anyMatch(p -> p.equals(path)); + } catch (Exception e) { + log.error("Fail Validate Path [{}] ClientProfile.Attribute", path, e); + return false; + } + } + + /** + * @param clientProfile - + * @param path - + * @return true if path isPresent in postAttributeProfile + */ + private boolean validatePathInTelemetryProfile(LwM2mClientProfile clientProfile, String path) { + try { + List telemetriesSet = new Gson().fromJson(clientProfile.getPostTelemetryProfile(), new TypeToken<>() { + }.getType()); + return telemetriesSet.stream().anyMatch(p -> p.equals(path)); + } catch (Exception e) { + log.error("Fail Validate Path [{}] ClientProfile.Telemetry", path, e); + return false; + } + } + + /** + * Start observe/read: Attr/Telemetry + * #1 - Analyze: + * #1.1 path in resource profile == client resource + * + * @param registration - + */ + private void initReadAttrTelemetryObserveToClient(Registration registration, LwM2mClient lwM2MClient, String typeOper) { + LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); + Set clientInstances = this.getAllInstancesInClient(registration); + Set result; + if (GET_TYPE_OPER_READ.equals(typeOper)) { + result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() {}); + result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() {})); + } else { + result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() {}); + } + Set pathSent = ConcurrentHashMap.newKeySet(); + result.forEach(target -> { + // #1.1 + String[] resPath = target.split("/"); + String instance = "/" + resPath[1] + "/" + resPath[2]; + if (clientInstances != null && clientInstances.size() > 0 && clientInstances.contains(instance)) { + pathSent.add(target); + } + }); + lwM2MClient.getPendingRequests().addAll(pathSent); + pathSent.forEach(target -> lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, ContentFormat.TLV.getName(), + null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout())); + if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { + lwM2MClient.initValue(this, null); + } + } + + /** + * Update parameters device in LwM2MClient + * If new deviceProfile != old deviceProfile => update deviceProfile + * + * @param registrationId - + * @param device - + */ + private void onDeviceUpdateLwM2MClient(String registrationId, Device device, Optional deviceProfileOpt) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClients().get(registrationId); + lwM2MClient.setDeviceName(device.getName()); + if (!lwM2MClient.getProfileId().equals(device.getDeviceProfileId().getId())) { + Set registrationIds = new HashSet<>(); + registrationIds.add(registrationId); + deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceUpdateChangeProfile(registrationIds, deviceProfile)); + } + + lwM2MClient.setProfileId(device.getDeviceProfileId().getId()); + } + + /** + * @param registration - + * @return - all object in client + */ + private Set getAllOjectsInClient(Registration registration) { + Set clientObjects = ConcurrentHashMap.newKeySet(); + Arrays.stream(registration.getObjectLinks()).forEach(url -> { + LwM2mPath pathIds = new LwM2mPath(url.getUrl()); + if (pathIds.isObjectInstance()) { + clientObjects.add("/" + pathIds.getObjectId()); + } + }); + return (clientObjects.size() > 0) ? clientObjects : null; + } + + /** + * @param registration - + * @return all instances in client + */ + private Set getAllInstancesInClient(Registration registration) { + Set clientInstances = ConcurrentHashMap.newKeySet(); + Arrays.stream(registration.getObjectLinks()).forEach(url -> { + LwM2mPath pathIds = new LwM2mPath(url.getUrl()); + if (pathIds.isObjectInstance()) { + clientInstances.add(url.getUrl()); + } + }); + return (clientInstances.size() > 0) ? clientInstances : null; + } + +// /** +// * get AttrName/TelemetryName with value from Client +// * +// * @param registration - +// * @return - JsonObject, format: {name: value}} +// */ +// private JsonObject getAttributeClient(Registration registration) { +// if (registration.getAdditionalRegistrationAttributes().size() > 0) { +// JsonObject resNameValues = new JsonObject(); +// registration.getAdditionalRegistrationAttributes().forEach(resNameValues::addProperty); +// return resNameValues; +// } +// return null; +// } + + /** + * @param attributes - new JsonObject + * @param telemetry - new JsonObject + * @param registration - Registration LwM2M Client + * @param path - + */ + private void getParametersFromProfile(JsonObject attributes, JsonObject telemetry, Registration registration, Set path) { + LwM2mClientProfile lwM2MClientProfile = lwM2mClientContext.getProfile(registration); + lwM2MClientProfile.getPostAttributeProfile().forEach(p -> { + LwM2mPath pathIds = new LwM2mPath(p.getAsString()); + if (pathIds.isResource()) { + if (path == null || path.contains(p.getAsString())) { + this.addParameters(p.getAsString(), attributes, registration); + } + } + }); + lwM2MClientProfile.getPostTelemetryProfile().forEach(p -> { + LwM2mPath pathIds = new LwM2mPath(p.getAsString()); + if (pathIds.isResource()) { + if (path == null || path.contains(p.getAsString())) { + this.addParameters(p.getAsString(), telemetry, registration); + } + } + }); + } + + /** + * @param parameters - JsonObject attributes/telemetry + * @param registration - Registration LwM2M Client + */ + private void addParameters(String path, JsonObject parameters, Registration registration) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + JsonObject names = lwM2mClientContext.getProfiles().get(lwM2MClient.getProfileId()).getPostKeyNameProfile(); + String resName = String.valueOf(names.get(path)); + if (resName != null && !resName.isEmpty()) { + try { + String resValue = this.getResourceValueToString(lwM2MClient, path); + if (resValue != null) { + parameters.addProperty(resName, resValue); + } + } catch (Exception e) { + log.error("Failed to add parameters.", e); + } + } + } + + /** + * @param path - path resource + * @return - value of Resource or null + */ + private String getResourceValueToString(LwM2mClient lwM2MClient, String path) { + LwM2mPath pathIds = new LwM2mPath(path); + ResourceValue resourceValue = this.returnResourceValueFromLwM2MClient(lwM2MClient, pathIds); + return resourceValue == null ? null : + this.converter.convertValue(resourceValue.getResourceValue(), this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModelType(lwM2MClient.getRegistration(), pathIds), ResourceModel.Type.STRING, pathIds).toString(); + } + + /** + * + * @param lwM2MClient - + * @param pathIds - + * @return - return value of Resource by idPath + */ + private ResourceValue returnResourceValueFromLwM2MClient(LwM2mClient lwM2MClient, LwM2mPath pathIds) { + ResourceValue resourceValue = null; + if (pathIds.isResource()) { + resourceValue = lwM2MClient.getResources().get(pathIds.toString()); + } + return resourceValue; + } + + /** + * Update resource (attribute) value on thingsboard after update value in client + * + * @param registration - + * @param path - + * @param request - + */ + public void onWriteResponseOk(Registration registration, String path, WriteRequest request) { + this.updateResourcesValue(registration, ((LwM2mResource) request.getNode()), path); + } + + /** + * #1 Read new, old Value (Attribute, Telemetry, Observe, KeyName) + * #2 Update in lwM2MClient: ...Profile if changes from update device + * #3 Equivalence test: old <> new Value (Attribute, Telemetry, Observe, KeyName) + * #3.1 Attribute isChange (add&del) + * #3.2 Telemetry isChange (add&del) + * #3.3 KeyName isChange (add) + * #4 update + * #4.1 add If #3 isChange, then analyze and update Value in Transport form Client and sent Value to thingsboard + * #4.2 del + * -- if add attributes includes del telemetry - result del for observe + * #5 + * #5.1 Observe isChange (add&del) + * #5.2 Observe.add + * -- path Attr/Telemetry includes newObserve and does not include oldObserve: sent Request observe to Client + * #5.3 Observe.del + * -- different between newObserve and oldObserve: sent Request cancel observe to client + * + * @param registrationIds - + * @param deviceProfile - + */ + private void onDeviceUpdateChangeProfile(Set registrationIds, DeviceProfile deviceProfile) { + LwM2mClientProfile lwM2MClientProfileOld = lwM2mClientContext.getProfiles().get(deviceProfile.getUuidId()).clone(); + if (lwM2mClientContext.addUpdateProfileParameters(deviceProfile)) { + // #1 + JsonArray attributeOld = lwM2MClientProfileOld.getPostAttributeProfile(); + Set attributeSetOld = this.convertJsonArrayToSet(attributeOld); + JsonArray telemetryOld = lwM2MClientProfileOld.getPostTelemetryProfile(); + Set telemetrySetOld = this.convertJsonArrayToSet(telemetryOld); + JsonArray observeOld = lwM2MClientProfileOld.getPostObserveProfile(); + JsonObject keyNameOld = lwM2MClientProfileOld.getPostKeyNameProfile(); + + LwM2mClientProfile lwM2MClientProfileNew = lwM2mClientContext.getProfiles().get(deviceProfile.getUuidId()); + JsonArray attributeNew = lwM2MClientProfileNew.getPostAttributeProfile(); + Set attributeSetNew = this.convertJsonArrayToSet(attributeNew); + JsonArray telemetryNew = lwM2MClientProfileNew.getPostTelemetryProfile(); + Set telemetrySetNew = this.convertJsonArrayToSet(telemetryNew); + JsonArray observeNew = lwM2MClientProfileNew.getPostObserveProfile(); + JsonObject keyNameNew = lwM2MClientProfileNew.getPostKeyNameProfile(); + + // #3 + ResultsAnalyzerParameters sentAttrToThingsboard = new ResultsAnalyzerParameters(); + // #3.1 + if (!attributeOld.equals(attributeNew)) { + ResultsAnalyzerParameters postAttributeAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(attributeOld, new TypeToken>() { + }.getType()), attributeSetNew); + sentAttrToThingsboard.getPathPostParametersAdd().addAll(postAttributeAnalyzer.getPathPostParametersAdd()); + sentAttrToThingsboard.getPathPostParametersDel().addAll(postAttributeAnalyzer.getPathPostParametersDel()); + } + // #3.2 + if (!telemetryOld.equals(telemetryNew)) { + ResultsAnalyzerParameters postTelemetryAnalyzer = this.getAnalyzerParameters(new Gson().fromJson(telemetryOld, new TypeToken>() { + }.getType()), telemetrySetNew); + sentAttrToThingsboard.getPathPostParametersAdd().addAll(postTelemetryAnalyzer.getPathPostParametersAdd()); + sentAttrToThingsboard.getPathPostParametersDel().addAll(postTelemetryAnalyzer.getPathPostParametersDel()); + } + // #3.3 + if (!keyNameOld.equals(keyNameNew)) { + ResultsAnalyzerParameters keyNameChange = this.getAnalyzerKeyName(new Gson().fromJson(keyNameOld.toString(), new TypeToken>() { + }.getType()), + new Gson().fromJson(keyNameNew.toString(), new TypeToken>() { + }.getType())); + sentAttrToThingsboard.getPathPostParametersAdd().addAll(keyNameChange.getPathPostParametersAdd()); + } + + // #4.1 add + if (sentAttrToThingsboard.getPathPostParametersAdd().size() > 0) { + // update value in Resources + registrationIds.forEach(registrationId -> { +// LeshanServer lwServer = leshanServer; + Registration registration = lwM2mClientContext.getRegistration(registrationId); + this.readResourceValueObserve(registration, sentAttrToThingsboard.getPathPostParametersAdd(), GET_TYPE_OPER_READ); + // sent attr/telemetry to tingsboard for new path + this.updateAttrTelemetry(registration, sentAttrToThingsboard.getPathPostParametersAdd()); + }); + } + // #4.2 del + if (sentAttrToThingsboard.getPathPostParametersDel().size() > 0) { + ResultsAnalyzerParameters sentAttrToThingsboardDel = this.getAnalyzerParameters(sentAttrToThingsboard.getPathPostParametersAdd(), sentAttrToThingsboard.getPathPostParametersDel()); + sentAttrToThingsboard.setPathPostParametersDel(sentAttrToThingsboardDel.getPathPostParametersDel()); + } + + // #5.1 + if (!observeOld.equals(observeNew)) { + Set observeSetOld = new Gson().fromJson(observeOld, new TypeToken<>() {}.getType()); + Set observeSetNew = new Gson().fromJson(observeNew, new TypeToken<>() {}.getType()); + //#5.2 add + // path Attr/Telemetry includes newObserve + attributeSetOld.addAll(telemetrySetOld); + ResultsAnalyzerParameters sentObserveToClientOld = this.getAnalyzerParametersIn(attributeSetOld, observeSetOld); // add observe + attributeSetNew.addAll(telemetrySetNew); + ResultsAnalyzerParameters sentObserveToClientNew = this.getAnalyzerParametersIn(attributeSetNew, observeSetNew); // add observe + // does not include oldObserve + ResultsAnalyzerParameters postObserveAnalyzer = this.getAnalyzerParameters(sentObserveToClientOld.getPathPostParametersAdd(), sentObserveToClientNew.getPathPostParametersAdd()); + // sent Request observe to Client + registrationIds.forEach(registrationId -> { + Registration registration = lwM2mClientContext.getRegistration(registrationId); + this.readResourceValueObserve(registration, postObserveAnalyzer.getPathPostParametersAdd(), GET_TYPE_OPER_OBSERVE); + // 5.3 del + // sent Request cancel observe to Client + this.cancelObserveIsValue(registration, postObserveAnalyzer.getPathPostParametersDel()); + }); + } + } + } + + private Set convertJsonArrayToSet (JsonArray jsonArray) { + List attributeListOld = new Gson().fromJson(jsonArray, new TypeToken<>() { + }.getType()); + return Sets.newConcurrentHashSet(attributeListOld); + } + + /** + * Compare old list with new list after change AttrTelemetryObserve in config Profile + * + * @param parametersOld - + * @param parametersNew - + * @return ResultsAnalyzerParameters: add && new + */ + private ResultsAnalyzerParameters getAnalyzerParameters(Set parametersOld, Set parametersNew) { + ResultsAnalyzerParameters analyzerParameters = null; + if (!parametersOld.equals(parametersNew)) { + analyzerParameters = new ResultsAnalyzerParameters(); + analyzerParameters.setPathPostParametersAdd(parametersNew + .stream().filter(p -> !parametersOld.contains(p)).collect(Collectors.toSet())); + analyzerParameters.setPathPostParametersDel(parametersOld + .stream().filter(p -> !parametersNew.contains(p)).collect(Collectors.toSet())); + } + return analyzerParameters; + } + + private ResultsAnalyzerParameters getAnalyzerParametersIn(Set parametersObserve, Set parameters) { + ResultsAnalyzerParameters analyzerParameters = new ResultsAnalyzerParameters(); + analyzerParameters.setPathPostParametersAdd(parametersObserve + .stream().filter(parameters::contains).collect(Collectors.toSet())); + return analyzerParameters; + } + + /** + * Update Resource value after change RezAttrTelemetry in config Profile + * sent response Read to Client and add path to pathResAttrTelemetry in LwM2MClient.getAttrTelemetryObserveValue() + * + * @param registration - Registration LwM2M Client + * @param targets - path Resources == [ "/2/0/0", "/2/0/1"] + */ + private void readResourceValueObserve(Registration registration, Set targets, String typeOper) { + targets.forEach(target -> { + LwM2mPath pathIds = new LwM2mPath(target); + if (pathIds.isResource()) { + if (GET_TYPE_OPER_READ.equals(typeOper)) { + lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, + ContentFormat.TLV.getName(), null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + } else if (GET_TYPE_OPER_OBSERVE.equals(typeOper)) { + lwM2mTransportRequest.sendAllRequest(registration, target, typeOper, + null, null, null, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + } + } + }); + } + + private ResultsAnalyzerParameters getAnalyzerKeyName(ConcurrentMap keyNameOld, ConcurrentMap keyNameNew) { + ResultsAnalyzerParameters analyzerParameters = new ResultsAnalyzerParameters(); + Set paths = keyNameNew.entrySet() + .stream() + .filter(e -> !e.getValue().equals(keyNameOld.get(e.getKey()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)).keySet(); + analyzerParameters.setPathPostParametersAdd(paths); + return analyzerParameters; + } + + private void cancelObserveIsValue(Registration registration, Set paramAnallyzer) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + paramAnallyzer.forEach(p -> { + if (this.returnResourceValueFromLwM2MClient(lwM2MClient, new LwM2mPath(p)) != null) { + this.setCancelObservationRecourse(registration, p); + } + } + ); + } + + private void putDelayedUpdateResourcesClient(LwM2mClient lwM2MClient, Object valueOld, Object valueNew, String path) { + if (valueNew != null && (valueOld == null || !valueNew.toString().equals(valueOld.toString()))) { + lwM2mTransportRequest.sendAllRequest(lwM2MClient.getRegistration(), path, POST_TYPE_OPER_WRITE_REPLACE, + ContentFormat.TLV.getName(), null, valueNew, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getTimeout()); + } else { + log.error("05 delayError"); + } + } + + /** + * @param updateCredentials - Credentials include config only security Client (without config attr/telemetry...) + * config attr/telemetry... in profile + */ + public void onToTransportUpdateCredentials(TransportProtos.ToTransportUpdateCredentialsProto updateCredentials) { + log.info("[{}] idList [{}] valueList updateCredentials", updateCredentials.getCredentialsIdList(), updateCredentials.getCredentialsValueList()); + } + + /** + * Get path to resource from profile equal keyName or from ModelObject equal name + * Only for resource: isWritable && isPresent as attribute in profile -> LwM2MClientProfile (format: CamelCase) + * + * @param sessionInfo - + * @param name - + * @return path if path isPresent in postProfile + */ + private String getPathAttributeUpdate(TransportProtos.SessionInfoProto sessionInfo, String name) { + String profilePath = this.getPathAttributeUpdateProfile(sessionInfo, name); + return !profilePath.isEmpty() ? profilePath : null; + } + + /** + * Get path to resource from profile equal keyName + * + * @param sessionInfo - + * @param name - + * @return - + */ + private String getPathAttributeUpdateProfile(TransportProtos.SessionInfoProto sessionInfo, String name) { + LwM2mClientProfile profile = lwM2mClientContext.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); + return profile.getPostKeyNameProfile().getAsJsonObject().entrySet().stream() + .filter(e -> e.getValue().getAsString().equals(name)).findFirst().map(Map.Entry::getKey) + .orElse(""); + } + + /** + * Update resource value on client: if there is a difference in values between the current resource values and the shared attribute values + * #1 Get path resource by result attributesResponse + * #1.1 If two names have equal path => last time attribute + * #2.1 if there is a difference in values between the current resource values and the shared attribute values + * => sent to client Request Update of value (new value from shared attribute) + * and LwM2MClient.delayedRequests.add(path) + * #2.1 if there is not a difference in values between the current resource values and the shared attribute values + * + * @param attributesResponse - + * @param sessionInfo - + */ + public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg attributesResponse, TransportProtos.SessionInfoProto sessionInfo) { + try { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2MClient(sessionInfo); + attributesResponse.getSharedAttributeListList().forEach(attr -> { + String path = this.getPathAttributeUpdate(sessionInfo, attr.getKv().getKey()); + // #1.1 + if (lwM2MClient.getDelayedRequests().containsKey(path) && attr.getTs() > lwM2MClient.getDelayedRequests().get(path).getTs()) { + lwM2MClient.getDelayedRequests().put(path, attr); + } else { + lwM2MClient.getDelayedRequests().put(path, attr); + } + }); + // #2.1 + lwM2MClient.getDelayedRequests().forEach((k, v) -> { + ArrayList listV = new ArrayList<>(); + listV.add(v.getKv()); + this.putDelayedUpdateResourcesClient(lwM2MClient, this.getResourceValueToString(lwM2MClient, k), getJsonObject(listV).get(v.getKv().getKey()), k); + }); + } catch (Exception e) { + log.error(String.valueOf(e)); + } + } + + /** + * @param lwM2MClient - + * @return SessionInfoProto - + */ + private SessionInfoProto getNewSessionInfoProto(LwM2mClient lwM2MClient) { + if (lwM2MClient != null) { + TransportProtos.ValidateDeviceCredentialsResponseMsg msg = lwM2MClient.getCredentialsResponse(); + if (msg == null) { + log.error("[{}] [{}]", lwM2MClient.getEndpoint(), CLIENT_NOT_AUTHORIZED); + this.closeClientSession(lwM2MClient.getRegistration()); + return null; + } else { + return SessionInfoProto.newBuilder() + .setNodeId(this.lwM2mTransportContextServer.getNodeId()) + .setSessionIdMSB(lwM2MClient.getSessionId().getMostSignificantBits()) + .setSessionIdLSB(lwM2MClient.getSessionId().getLeastSignificantBits()) + .setDeviceIdMSB(msg.getDeviceInfo().getDeviceIdMSB()) + .setDeviceIdLSB(msg.getDeviceInfo().getDeviceIdLSB()) + .setTenantIdMSB(msg.getDeviceInfo().getTenantIdMSB()) + .setTenantIdLSB(msg.getDeviceInfo().getTenantIdLSB()) + .setDeviceName(msg.getDeviceInfo().getDeviceName()) + .setDeviceType(msg.getDeviceInfo().getDeviceType()) + .setDeviceProfileIdLSB(msg.getDeviceInfo().getDeviceProfileIdLSB()) + .setDeviceProfileIdMSB(msg.getDeviceInfo().getDeviceProfileIdMSB()) + .build(); + } + } + return null; + } + + /** + * @param registration - Registration LwM2M Client + * @return - sessionInfo after access connect client + */ + private SessionInfoProto getValidateSessionInfo(Registration registration) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(registration, null); + return getNewSessionInfoProto(lwM2MClient); + } + + /** + * @param registrationId - + * @return - + */ + private SessionInfoProto getValidateSessionInfo(String registrationId) { + LwM2mClient lwM2MClient = lwM2mClientContext.getLwM2mClientWithReg(null, registrationId); + return getNewSessionInfoProto(lwM2MClient); + } + + /** + * if sessionInfo removed from sessions, then new registerAsyncSession + * + * @param sessionInfo - + */ + private void checkInactivity(SessionInfoProto sessionInfo) { + if (transportService.reportActivity(sessionInfo) == null) { + transportService.registerAsyncSession(sessionInfo, new LwM2mSessionMsgListener(this, sessionInfo)); + } + } + + private void checkInactivityAndReportActivity() { + lwM2mClientContext.getLwM2mClients().forEach((key, value) -> this.checkInactivity(this.getValidateSessionInfo(key))); + } + + /** + * If there is a difference in values between the current resource values and the shared attribute values + * when the client connects to the server + * #1 get attributes name from profile include name resources in ModelObject if resource isWritable + * #2.1 #1 size > 0 => send Request getAttributes to thingsboard + * + * @param lwM2MClient - LwM2M Client + */ + public void putDelayedUpdateResourcesThingsboard(LwM2mClient lwM2MClient) { + SessionInfoProto sessionInfo = this.getValidateSessionInfo(lwM2MClient.getRegistration()); + if (sessionInfo != null) { + //#1.1 + #1.2 + List attrSharedNames = this.getNamesAttrFromProfileIsWritable(lwM2MClient); + if (attrSharedNames.size() > 0) { + //#2.1 + try { + TransportProtos.GetAttributeRequestMsg getAttributeMsg = lwM2mTransportContextServer.getAdaptor().convertToGetAttributes(null, attrSharedNames); + transportService.process(sessionInfo, getAttributeMsg, getAckCallback(lwM2MClient, getAttributeMsg.getRequestId(), DEVICE_ATTRIBUTES_REQUEST)); + } catch (AdaptorException e) { + log.warn("Failed to decode get attributes request", e); + } + } + } + } + + + /** + * Get names and keyNames from profile shared!!!! attr resources IsWritable + * + * @param lwM2MClient - + * @return ArrayList keyNames from profile attr resources shared!!!! && IsWritable + */ + private List getNamesAttrFromProfileIsWritable(LwM2mClient lwM2MClient) { + LwM2mClientProfile profile = lwM2mClientContext.getProfile(lwM2MClient.getProfileId()); + Set attrSet = new Gson().fromJson(profile.getPostAttributeProfile(), new TypeToken<>() {}.getType()); + ConcurrentMap keyNamesMap = new Gson().fromJson(profile.getPostKeyNameProfile().toString(), new TypeToken>() {}.getType()); + + ConcurrentMap keyNamesIsWritable = keyNamesMap.entrySet() + .stream() + .filter(e -> (attrSet.contains(e.getKey()) && lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(lwM2MClient.getRegistration(), new LwM2mPath(e.getKey())) != null && + lwM2mTransportContextServer.getLwM2MTransportConfigServer().getResourceModel(lwM2MClient.getRegistration(), new LwM2mPath(e.getKey())).operations.isWritable())) + .collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue)); + + Set namesIsWritable = ConcurrentHashMap.newKeySet(); + namesIsWritable.addAll(new HashSet<>(keyNamesIsWritable.values())); + return new ArrayList<>(namesIsWritable); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java new file mode 100644 index 0000000000..b228a52b94 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MJsonAdaptor.java @@ -0,0 +1,88 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.adaptors; + +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +@Slf4j +@Component("LwM2MJsonAdaptor") +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true' )|| ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled}'=='true')") +public class LwM2MJsonAdaptor implements LwM2MTransportAdaptor { + + @Override + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(JsonElement jsonElement) throws AdaptorException { + try { + return JsonConverter.convertToTelemetryProto(jsonElement); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + + @Override + public TransportProtos.PostAttributeMsg convertToPostAttributes(JsonElement jsonElement) throws AdaptorException { + try { + return JsonConverter.convertToAttributesProto(jsonElement); + } catch (IllegalStateException | JsonSyntaxException ex) { + throw new AdaptorException(ex); + } + } + + @Override + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(List clientKeys, List sharedKeys) throws AdaptorException { + return processGetAttributeRequestMsg(clientKeys, sharedKeys); + } + + protected TransportProtos.GetAttributeRequestMsg processGetAttributeRequestMsg(List clientKeys, List sharedKeys) throws AdaptorException { + try { + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); + Random random = new Random(); + result.setRequestId(random.nextInt()); + if (clientKeys != null) { + result.addAllClientAttributeNames(clientKeys); + } + if (sharedKeys != null) { + result.addAllSharedAttributeNames(sharedKeys); + } + return result.build(); + } catch (RuntimeException e) { + log.warn("Failed to decode get attributes request", e); + throw new AdaptorException(e); + } + } + + private Set toStringSet(JsonElement requestBody, String name) { + JsonElement element = requestBody.getAsJsonObject().get(name); + if (element != null) { + return new HashSet<>(Arrays.asList(element.getAsString().split(","))); + } else { + return null; + } + } + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MTransportAdaptor.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MTransportAdaptor.java new file mode 100644 index 0000000000..4b6d54c874 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/adaptors/LwM2MTransportAdaptor.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.adaptors; + +import com.google.gson.JsonElement; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.List; + +public interface LwM2MTransportAdaptor { + + TransportProtos.PostTelemetryMsg convertToPostTelemetry(JsonElement jsonElement) throws AdaptorException; + + TransportProtos.PostAttributeMsg convertToPostAttributes(JsonElement jsonElement) throws AdaptorException; + + TransportProtos.GetAttributeRequestMsg convertToGetAttributes(List clientKeys, List sharedKeys) throws AdaptorException; +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java new file mode 100644 index 0000000000..1c9afaab6d --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -0,0 +1,92 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.node.LwM2mMultipleResource; +import org.eclipse.leshan.core.node.LwM2mResource; +import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +@Slf4j +@Data +public class LwM2mClient implements Cloneable { + private String deviceName; + private String deviceProfileName; + private String endpoint; + private String identity; + private SecurityInfo securityInfo; + private UUID deviceId; + private UUID sessionId; + private UUID profileId; + private Registration registration; + private ValidateDeviceCredentialsResponseMsg credentialsResponse; + private final Map resources; + private final Map delayedRequests; + private final List pendingRequests; + private boolean init; + + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public LwM2mClient(String endpoint, String identity, SecurityInfo securityInfo, ValidateDeviceCredentialsResponseMsg credentialsResponse, UUID profileId, UUID sessionId) { + this.endpoint = endpoint; + this.identity = identity; + this.securityInfo = securityInfo; + this.credentialsResponse = credentialsResponse; + this.delayedRequests = new ConcurrentHashMap<>(); + this.pendingRequests = new CopyOnWriteArrayList<>(); + this.resources = new ConcurrentHashMap<>(); + this.profileId = profileId; + this.sessionId = sessionId; + this.init = false; + } + + public void updateResourceValue(String pathRez, LwM2mResource rez) { + if (rez instanceof LwM2mMultipleResource) { + this.resources.put(pathRez, new ResourceValue(rez.getValues(), null, true)); + } else if (rez instanceof LwM2mSingleResource) { + this.resources.put(pathRez, new ResourceValue(null, rez.getValue(), false)); + } + } + + public void initValue(LwM2mTransportServiceImpl lwM2MTransportService, String path) { + if (path != null) { + this.pendingRequests.remove(path); + } + if (this.pendingRequests.size() == 0) { + this.init = true; + lwM2MTransportService.putDelayedUpdateResourcesThingsboard(this); + } + } + + public LwM2mClient copy() { + return new LwM2mClient(this.endpoint, this.identity, this.securityInfo, this.credentialsResponse, this.profileId, this.sessionId); + } +} + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java new file mode 100644 index 0000000000..2aea3fdc3e --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import org.eclipse.leshan.server.registration.Registration; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.util.Map; +import java.util.UUID; + +public interface LwM2mClientContext { + + void delRemoveSessionAndListener(String registrationId); + + LwM2mClient getLwM2MClient(String endPoint, String identity); + + LwM2mClient getLwM2MClient(TransportProtos.SessionInfoProto sessionInfo); + + LwM2mClient getLwM2mClient(UUID sessionId); + + LwM2mClient getLwM2mClientWithReg(Registration registration, String registrationId); + + LwM2mClient updateInSessionsLwM2MClient(Registration registration); + + LwM2mClient addLwM2mClientToSession(String identity); + + Registration getRegistration(String registrationId); + + Map getLwM2mClients(); + + Map getProfiles(); + + LwM2mClientProfile getProfile(UUID profileUuId); + + LwM2mClientProfile getProfile(Registration registration); + + Map setProfiles(Map profiles); + + boolean addUpdateProfileParameters(DeviceProfile deviceProfile); +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java new file mode 100644 index 0000000000..b965c99611 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -0,0 +1,171 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.security.EditableSecurityStore; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; +import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; +import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; +import org.thingsboard.server.transport.lwm2m.utils.TypeServer; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; + +@Service +@TbLwM2mTransportComponent +public class LwM2mClientContextImpl implements LwM2mClientContext { + private static final boolean INFOS_ARE_COMPROMISED = false; + + private final Map lwM2mClients = new ConcurrentHashMap<>(); + private Map profiles = new ConcurrentHashMap<>(); + + private final LwM2mCredentialsSecurityInfoValidator lwM2MCredentialsSecurityInfoValidator; + + private final EditableSecurityStore securityStore; + + public LwM2mClientContextImpl(LwM2mCredentialsSecurityInfoValidator lwM2MCredentialsSecurityInfoValidator, EditableSecurityStore securityStore) { + this.lwM2MCredentialsSecurityInfoValidator = lwM2MCredentialsSecurityInfoValidator; + this.securityStore = securityStore; + } + + public void delRemoveSessionAndListener(String registrationId) { + LwM2mClient lwM2MClient = lwM2mClients.get(registrationId); + if (lwM2MClient != null) { + securityStore.remove(lwM2MClient.getEndpoint(), INFOS_ARE_COMPROMISED); + lwM2mClients.remove(registrationId); + } + } + + @Override + public LwM2mClient getLwM2MClient(String endPoint, String identity) { + Map.Entry modelClients = endPoint != null ? + this.lwM2mClients.entrySet().stream().filter(model -> endPoint.equals(model.getValue().getEndpoint())).findAny().orElse(null) : + this.lwM2mClients.entrySet().stream().filter(model -> identity.equals(model.getValue().getIdentity())).findAny().orElse(null); + return modelClients != null ? modelClients.getValue() : null; + } + + @Override + public LwM2mClient getLwM2MClient(TransportProtos.SessionInfoProto sessionInfo) { + return getLwM2mClient(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())); + } + + @Override + public LwM2mClient getLwM2mClient(UUID sessionId) { + return lwM2mClients.values().stream().filter(c -> c.getSessionId().equals(sessionId)).findAny().get(); + } + + @Override + public LwM2mClient getLwM2mClientWithReg(Registration registration, String registrationId) { + LwM2mClient client = registrationId != null ? + this.lwM2mClients.get(registrationId) : + this.lwM2mClients.containsKey(registration.getId()) ? + this.lwM2mClients.get(registration.getId()) : + this.lwM2mClients.get(registration.getEndpoint()); + return client != null ? client : updateInSessionsLwM2MClient(registration); + } + + @Override + public LwM2mClient updateInSessionsLwM2MClient(Registration registration) { + if (this.lwM2mClients.get(registration.getEndpoint()) == null) { + addLwM2mClientToSession(registration.getEndpoint()); + } + LwM2mClient lwM2MClient = lwM2mClients.get(registration.getEndpoint()); + lwM2MClient.setRegistration(registration); + this.lwM2mClients.remove(registration.getEndpoint()); + this.lwM2mClients.put(registration.getId(), lwM2MClient); + return lwM2MClient; + } + + public Registration getRegistration(String registrationId) { + return this.lwM2mClients.get(registrationId).getRegistration(); + } + + /** + * Add new LwM2MClient to session + * @param identity- + * @return SecurityInfo. If error - SecurityInfoError + * and log: + * - FORBIDDEN - if there is no authorization + * - profileUuid - if the device does not have a profile + * - device - if the thingsboard does not have a device with a name equal to the identity + */ + @Override + public LwM2mClient addLwM2mClientToSession(String identity) { + ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(identity, TypeServer.CLIENT); + if (store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { + UUID profileUuid = (store.getDeviceProfile() != null && addUpdateProfileParameters(store.getDeviceProfile())) ? store.getDeviceProfile().getUuidId() : null; + LwM2mClient client; + if (store.getSecurityInfo() != null && profileUuid != null) { + String endpoint = store.getSecurityInfo().getEndpoint(); + client = new LwM2mClient(endpoint, store.getSecurityInfo().getIdentity(), store.getSecurityInfo(), store.getMsg(), profileUuid, UUID.randomUUID()); + lwM2mClients.put(endpoint, client); + } else if (store.getSecurityMode() == NO_SEC.code && profileUuid != null) { + client = new LwM2mClient(identity, null, null, store.getMsg(), profileUuid, UUID.randomUUID()); + lwM2mClients.put(identity, client); + } else { + throw new RuntimeException(String.format("Registration failed: FORBIDDEN/profileUuid/device %s , endpointId: %s [PSK]", profileUuid, identity)); + } + return client; + } else { + throw new RuntimeException(String.format("Registration failed: FORBIDDEN, endpointId: %s", identity)); + } + } + + @Override + public Map getLwM2mClients() { + return lwM2mClients; + } + + @Override + public Map getProfiles() { + return profiles; + } + + @Override + public LwM2mClientProfile getProfile(UUID profileId) { + return profiles.get(profileId); + } + + @Override + public LwM2mClientProfile getProfile(Registration registration) { + return this.getProfiles().get(getLwM2mClientWithReg(registration, null).getProfileId()); + } + + @Override + public Map setProfiles(Map profiles) { + return this.profiles = profiles; + } + + @Override + public boolean addUpdateProfileParameters(DeviceProfile deviceProfile) { + LwM2mClientProfile lwM2MClientProfile = LwM2mTransportHandler.getLwM2MClientProfileFromThingsboard(deviceProfile); + if (lwM2MClientProfile != null) { + profiles.put(deviceProfile.getUuidId(), lwM2MClientProfile); + return true; + } + return false; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java new file mode 100644 index 0000000000..810ea253a7 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientProfile.java @@ -0,0 +1,78 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Data; + +@Data +public class LwM2mClientProfile { + /** + * {"clientLwM2mSettings": { + * clientUpdateValueAfterConnect: false; + * } + **/ + JsonObject postClientLwM2mSettings; + + /** + * {"keyName": { + * "/3/0/1": "modelNumber", + * "/3/0/0": "manufacturer", + * "/3/0/2": "serialNumber" + * } + **/ + JsonObject postKeyNameProfile; + + /** + * [ "/2/0/0", "/2/0/1"] + */ + JsonArray postAttributeProfile; + + /** + * [ "/2/0/0", "/2/0/1"] + */ + JsonArray postTelemetryProfile; + + /** + * [ "/2/0/0", "/2/0/1"] + */ + JsonArray postObserveProfile; + + public LwM2mClientProfile clone() { + LwM2mClientProfile lwM2mClientProfile = new LwM2mClientProfile(); + lwM2mClientProfile.postClientLwM2mSettings = this.deepCopy(this.postClientLwM2mSettings, JsonObject.class); + lwM2mClientProfile.postKeyNameProfile = this.deepCopy(this.postKeyNameProfile, JsonObject.class); + lwM2mClientProfile.postAttributeProfile = this.deepCopy(this.postAttributeProfile, JsonArray.class); + lwM2mClientProfile.postTelemetryProfile = this.deepCopy(this.postTelemetryProfile, JsonArray.class); + lwM2mClientProfile.postObserveProfile = this.deepCopy(this.postObserveProfile, JsonArray.class); + return lwM2mClientProfile; + } + + + private T deepCopy(T elements, Class type) { + try { + Gson gson = new Gson(); + return gson.fromJson(gson.toJson(elements), type); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ModelObject.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ModelObject.java new file mode 100644 index 0000000000..5d8fedc9c7 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ModelObject.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import lombok.Data; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.node.LwM2mObjectInstance; +import java.util.Map; + +@Data +public class ModelObject implements Cloneable { + /** + * model one on all instance + * for each instance only id resource with parameters of resources (observe, attr, telemetry) + */ + private ObjectModel objectModel; + private Map instances; + + public ModelObject(ObjectModel objectModel, Map instances) { + this.objectModel = objectModel; + this.instances = instances; + } + + public boolean removeInstance (int id ) { + LwM2mObjectInstance instance = this.instances.get(id); + return this.instances.remove(id, instance); + } + + public ModelObject clone() throws CloneNotSupportedException { + return (ModelObject) super.clone(); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java new file mode 100644 index 0000000000..3ff04f288b --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.client; + +import lombok.Data; + +import java.util.Map; + +@Data +public class ResourceValue { + Map values; + Object value; + boolean multiInstances; + + public ResourceValue(Map values, Object value, boolean multiInstances) { + this.values = values; + this.value = value; + this.multiInstances = multiInstances; + } + + public Object getResourceValue() { + return this.multiInstances ? this.values : this.value; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAnalyzerParameters.java similarity index 58% rename from application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java rename to common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAnalyzerParameters.java index b62c513780..45b52811d8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultsAnalyzerParameters.java @@ -13,25 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.actors.ruleChain; +package org.thingsboard.server.transport.lwm2m.server.client; import lombok.Data; -import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.TbActorMsg; -import org.thingsboard.server.common.msg.TbMsg; -/** - * Created by ashvayka on 19.03.18. - */ -@Data -final class RuleNodeToSelfErrorMsg implements TbActorMsg { +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; - private final TbMsg msg; - private final Throwable error; +@Data +public class ResultsAnalyzerParameters { + Set pathPostParametersAdd; + Set pathPostParametersDel; - @Override - public MsgType getMsgType() { - return MsgType.RULE_TO_SELF_ERROR_MSG; + public ResultsAnalyzerParameters() { + this.pathPostParametersAdd = ConcurrentHashMap.newKeySet(); + this.pathPostParametersDel = ConcurrentHashMap.newKeySet(); } - } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/LwM2mInMemorySecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/LwM2mInMemorySecurityStore.java new file mode 100644 index 0000000000..fc66a5fed0 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/LwM2mInMemorySecurityStore.java @@ -0,0 +1,251 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.store; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.security.InMemorySecurityStore; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.eclipse.leshan.server.security.SecurityStoreListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode; +import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator; +import org.thingsboard.server.transport.lwm2m.secure.ReadResultSecurityStore; +import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientProfile; +import org.thingsboard.server.transport.lwm2m.utils.TypeServer; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC; + +@Slf4j +//@Service("LwM2mInMemorySecurityStore") +//@TbLwM2mTransportComponent +@Deprecated +public class LwM2mInMemorySecurityStore extends InMemorySecurityStore { + private static final boolean INFOS_ARE_COMPROMISED = false; + + // lock for the two maps + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final Lock readLock = readWriteLock.readLock(); + private final Lock writeLock = readWriteLock.writeLock(); + private final Map sessions = new ConcurrentHashMap<>(); + private Map profiles = new ConcurrentHashMap<>(); + private SecurityStoreListener listener; + + @Autowired + LwM2mCredentialsSecurityInfoValidator lwM2MCredentialsSecurityInfoValidator; + + /** + * Start after DefaultAuthorizer or LwM2mPskStore + * @param endPoint - + * @return SecurityInfo + */ + @Override + public SecurityInfo getByEndpoint(String endPoint) { + readLock.lock(); + try { + String registrationId = this.getRegistrationId(endPoint, null); + return (registrationId != null && sessions.size() > 0 && sessions.get(registrationId) != null) ? + sessions.get(registrationId).getSecurityInfo() : this.addLwM2MClientToSession(endPoint); + } finally { + readLock.unlock(); + } + } + + /** + * Start after LwM2mPskStore + * @param identity - + * @return SecurityInfo + */ + @Override + public SecurityInfo getByIdentity(String identity) { + readLock.lock(); + try { + String integrationId = this.getRegistrationId(null, identity); + return (integrationId != null) ? sessions.get(integrationId).getSecurityInfo() : this.addLwM2MClientToSession(identity); + } finally { + readLock.unlock(); + } + } + + @Override + public Collection getAll() { + readLock.lock(); + try { + return this.sessions.values().stream().map(LwM2mClient::getSecurityInfo).collect(Collectors.toUnmodifiableList()); + } finally { + readLock.unlock(); + } + } + + /** + * Removed registration Client from sessions and listener + * @param registrationId if Client + */ + public void delRemoveSessionAndListener(String registrationId) { + writeLock.lock(); + try { + LwM2mClient lwM2MClient = (sessions.get(registrationId) != null) ? sessions.get(registrationId) : null; + if (lwM2MClient != null) { + if (listener != null) { + listener.securityInfoRemoved(INFOS_ARE_COMPROMISED, lwM2MClient.getSecurityInfo()); + } + sessions.remove(registrationId); + } + } finally { + writeLock.unlock(); + } + } + + @Override + public void setListener(SecurityStoreListener listener) { + this.listener = listener; + } + + public LwM2mClient getLwM2MClient(String endPoint, String identity) { + Map.Entry modelClients = endPoint != null ? + this.sessions.entrySet().stream().filter(model -> endPoint.equals(model.getValue().getEndpoint())).findAny().orElse(null) : + this.sessions.entrySet().stream().filter(model -> identity.equals(model.getValue().getIdentity())).findAny().orElse(null); + return modelClients != null ? modelClients.getValue() : null; + } + + public LwM2mClient getLwM2MClientWithReg(Registration registration, String registrationId) { + return registrationId != null ? + this.sessions.get(registrationId) : + this.sessions.containsKey(registration.getId()) ? + this.sessions.get(registration.getId()) : + this.sessions.get(registration.getEndpoint()); + } + + public LwM2mClient getLwM2MClient(TransportProtos.SessionInfoProto sessionInfo) { + return this.getSession(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())).entrySet().iterator().next().getValue(); + } + + /** + * Update in sessions (LwM2MClient for key registration_Id) after starting registration LwM2MClient in LwM2MTransportServiceImpl + * Remove from sessions LwM2MClient with key registration_Endpoint + * @param registration - + * @return LwM2MClient after adding it to session + */ + public LwM2mClient updateInSessionsLwM2MClient(Registration registration) { + writeLock.lock(); + try { + if (this.sessions.get(registration.getEndpoint()) == null) { + this.addLwM2MClientToSession(registration.getEndpoint()); + } + LwM2mClient lwM2MClient = this.sessions.get(registration.getEndpoint()); + lwM2MClient.setRegistration(registration); +// lwM2MClient.getAttributes().putAll(registration.getAdditionalRegistrationAttributes()); + this.sessions.remove(registration.getEndpoint()); + this.sessions.put(registration.getId(), lwM2MClient); + return lwM2MClient; + } finally { + writeLock.unlock(); + } + } + + private String getRegistrationId(String endPoint, String identity) { + List registrationIds = (endPoint != null) ? + this.sessions.entrySet().stream().filter(model -> endPoint.equals(model.getValue().getEndpoint())).map(Map.Entry::getKey).collect(Collectors.toList()) : + this.sessions.entrySet().stream().filter(model -> identity.equals(model.getValue().getIdentity())).map(Map.Entry::getKey).collect(Collectors.toList()); + return (registrationIds != null && registrationIds.size() > 0) ? registrationIds.get(0) : null; + } + + public Registration getByRegistration(String registrationId) { + return this.sessions.get(registrationId).getRegistration(); + } + + /** + * Add new LwM2MClient to session + * @param identity- + * @return SecurityInfo. If error - SecurityInfoError + * and log: + * - FORBIDDEN - if there is no authorization + * - profileUuid - if the device does not have a profile + * - device - if the thingsboard does not have a device with a name equal to the identity + */ + private SecurityInfo addLwM2MClientToSession(String identity) { + ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(identity, TypeServer.CLIENT); + if (store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { + UUID profileUuid = (store.getDeviceProfile() != null && addUpdateProfileParameters(store.getDeviceProfile())) ? store.getDeviceProfile().getUuidId() : null; + if (store.getSecurityInfo() != null && profileUuid != null) { + String endpoint = store.getSecurityInfo().getEndpoint(); + sessions.put(endpoint, new LwM2mClient(endpoint, store.getSecurityInfo().getIdentity(), store.getSecurityInfo(), store.getMsg(), profileUuid, UUID.randomUUID())); + } else if (store.getSecurityMode() == NO_SEC.code && profileUuid != null) { + sessions.put(identity, new LwM2mClient(identity, null, null, store.getMsg(), profileUuid, UUID.randomUUID())); + } else { + log.error("Registration failed: FORBIDDEN/profileUuid/device [{}] , endpointId: [{}]", profileUuid, identity); + /** + * Return Error securityInfo + */ + byte[] preSharedKey = Hex.decodeHex("0A0B".toCharArray()); + SecurityInfo infoError = SecurityInfo.newPreSharedKeyInfo("error", "error_identity", preSharedKey); + return infoError; + } + } + return store.getSecurityInfo(); + } + + public Map getSession(UUID sessionUuId) { + return this.sessions.entrySet().stream() + .filter(e -> e.getValue().getSessionId().equals(sessionUuId)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public Map getSessions() { + return this.sessions; + } + + public Map getProfiles() { + return this.profiles; + } + + public LwM2mClientProfile getProfile(UUID profileUuId) { + return this.profiles.get(profileUuId); + } + + public LwM2mClientProfile getProfile(String registrationId) { + UUID profileUUid = this.getSessions().get(registrationId).getProfileId(); + return this.getProfiles().get(profileUUid); + } + + public Map setProfiles(Map profiles) { + return this.profiles = profiles; + } + + public boolean addUpdateProfileParameters(DeviceProfile deviceProfile) { + LwM2mClientProfile lwM2MClientProfile = LwM2mTransportHandler.getLwM2MClientProfileFromThingsboard(deviceProfile); + if (lwM2MClientProfile != null) { + profiles.put(deviceProfile.getUuidId(), lwM2MClientProfile); + return true; + } + return false; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java new file mode 100644 index 0000000000..bac4554b04 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -0,0 +1,784 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.store; + +import org.eclipse.californium.core.coap.Token; +import org.eclipse.californium.core.observe.ObservationStoreException; +import org.eclipse.californium.elements.EndpointContext; +import org.eclipse.leshan.core.observation.Observation; +import org.eclipse.leshan.core.util.NamedThreadFactory; +import org.eclipse.leshan.core.util.Validate; +import org.eclipse.leshan.core.Destroyable; +import org.eclipse.leshan.core.Startable; +import org.eclipse.leshan.core.Stoppable; +import org.eclipse.leshan.server.californium.observation.ObserveUtil; +import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.eclipse.leshan.server.redis.JedisLock; +import org.eclipse.leshan.server.redis.RedisRegistrationStore; +import org.eclipse.leshan.server.redis.SingleInstanceJedisLock; +import org.eclipse.leshan.server.redis.serialization.ObservationSerDes; +import org.eclipse.leshan.server.redis.serialization.RegistrationSerDes; +import org.eclipse.leshan.server.registration.Deregistration; +import org.eclipse.leshan.server.registration.ExpirationListener; +import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationUpdate; +import org.eclipse.leshan.server.registration.UpdatedRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.ScanParams; +import redis.clients.jedis.ScanResult; +import redis.clients.jedis.Transaction; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class TbLwM2mRedisRegistrationStore implements CaliforniumRegistrationStore, Startable, Stoppable, Destroyable { + /** Default time in seconds between 2 cleaning tasks (used to remove expired registration). */ + public static final long DEFAULT_CLEAN_PERIOD = 60; + public static final int DEFAULT_CLEAN_LIMIT = 500; + /** Defaut Extra time for registration lifetime in seconds */ + public static final long DEFAULT_GRACE_PERIOD = 0; + + private static final Logger LOG = LoggerFactory.getLogger(RedisRegistrationStore.class); + + // Redis key prefixes + private static final String REG_EP = "REG:EP:"; // (Endpoint => Registration) + private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint) + private static final String REG_EP_ADDR_IDX = "EP:ADDR:"; // secondary index key (Socket Address => Endpoint) + private static final String LOCK_EP = "LOCK:EP:"; + private static final byte[] OBS_TKN = "OBS:TKN:".getBytes(UTF_8); + private static final String OBS_TKNS_REGID_IDX = "TKNS:REGID:"; // secondary index (token list by registration) + private static final byte[] EXP_EP = "EXP:EP".getBytes(UTF_8); // a sorted set used for registration expiration + // (expiration date, Endpoint) + + private final RedisConnectionFactory connectionFactory; + + // Listener use to notify when a registration expires + private ExpirationListener expirationListener; + + private final ScheduledExecutorService schedExecutor; + private ScheduledFuture cleanerTask; + private boolean started = false; + + private final long cleanPeriod; // in seconds + private final int cleanLimit; // maximum number to clean in a clean period + private final long gracePeriod; // in seconds + + private final JedisLock lock; + + public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory) { + this(connectionFactory, DEFAULT_CLEAN_PERIOD, DEFAULT_GRACE_PERIOD, DEFAULT_CLEAN_LIMIT); // default clean period 60s + } + + public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, long cleanPeriodInSec, long lifetimeGracePeriodInSec, int cleanLimit) { + this(connectionFactory, Executors.newScheduledThreadPool(1, + new NamedThreadFactory(String.format("RedisRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), + cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit); + } + + public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, + long lifetimeGracePeriodInSec, int cleanLimit) { + this(connectionFactory, schedExecutor, cleanPeriodInSec, lifetimeGracePeriodInSec, cleanLimit, new SingleInstanceJedisLock()); + } + + /** + * @since 1.1 + */ + public TbLwM2mRedisRegistrationStore(RedisConnectionFactory connectionFactory, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, + long lifetimeGracePeriodInSec, int cleanLimit, JedisLock redisLock) { + this.connectionFactory = connectionFactory; + this.schedExecutor = schedExecutor; + this.cleanPeriod = cleanPeriodInSec; + this.cleanLimit = cleanLimit; + this.gracePeriod = lifetimeGracePeriodInSec; + this.lock = redisLock; + } + + /* *************** Redis Key utility function **************** */ + + private byte[] toKey(byte[] prefix, byte[] key) { + byte[] result = new byte[prefix.length + key.length]; + System.arraycopy(prefix, 0, result, 0, prefix.length); + System.arraycopy(key, 0, result, prefix.length, key.length); + return result; + } + + private byte[] toKey(String prefix, String registrationID) { + return (prefix + registrationID).getBytes(); + } + + private byte[] toLockKey(String endpoint) { + return toKey(LOCK_EP, endpoint); + } + + private byte[] toLockKey(byte[] endpoint) { + return toKey(LOCK_EP.getBytes(UTF_8), endpoint); + } + + /* *************** Leshan Registration API **************** */ + + @Override + public Deregistration addRegistration(Registration registration) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] lockValue = null; + byte[] lockKey = toLockKey(registration.getEndpoint()); + + try { + lockValue = lock.acquire(j, lockKey); + + // add registration + byte[] k = toEndpointKey(registration.getEndpoint()); + byte[] old = j.getSet(k, serializeReg(registration)); + + // add registration: secondary indexes + byte[] regid_idx = toRegIdKey(registration.getId()); + j.set(regid_idx, registration.getEndpoint().getBytes(UTF_8)); + byte[] addr_idx = toRegAddrKey(registration.getSocketAddress()); + j.set(addr_idx, registration.getEndpoint().getBytes(UTF_8)); + + // Add or update expiration + addOrUpdateExpiration(j, registration); + + if (old != null) { + Registration oldRegistration = deserializeReg(old); + // remove old secondary index + if (!registration.getId().equals(oldRegistration.getId())) + j.del(toRegIdKey(oldRegistration.getId())); + if (!oldRegistration.getSocketAddress().equals(registration.getSocketAddress())) { + removeAddrIndex(j, oldRegistration); + } + // remove old observation + Collection obsRemoved = unsafeRemoveAllObservations(j, oldRegistration.getId()); + + return new Deregistration(oldRegistration, obsRemoved); + } + + return null; + } finally { + lock.release(j, lockKey, lockValue); + } + } + } + + @Override + public UpdatedRegistration updateRegistration(RegistrationUpdate update) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + + // Fetch the registration ep by registration ID index + byte[] ep = j.get(toRegIdKey(update.getRegistrationId())); + if (ep == null) { + return null; + } + + byte[] lockValue = null; + byte[] lockKey = toLockKey(ep); + try { + lockValue = lock.acquire(j, lockKey); + + // Fetch the registration + byte[] data = j.get(toEndpointKey(ep)); + if (data == null) { + return null; + } + + Registration r = deserializeReg(data); + + Registration updatedRegistration = update.update(r); + + // Store the new registration + j.set(toEndpointKey(updatedRegistration.getEndpoint()), serializeReg(updatedRegistration)); + + // Add or update expiration + addOrUpdateExpiration(j, updatedRegistration); + + // Update secondary index : + // If registration is already associated to this address we don't care as we only want to keep the most + // recent binding. + byte[] addr_idx = toRegAddrKey(updatedRegistration.getSocketAddress()); + j.set(addr_idx, updatedRegistration.getEndpoint().getBytes(UTF_8)); + if (!r.getSocketAddress().equals(updatedRegistration.getSocketAddress())) { + removeAddrIndex(j, r); + } + + return new UpdatedRegistration(r, updatedRegistration); + + } finally { + lock.release(j, lockKey, lockValue); + } + } + } + + @Override + public Registration getRegistration(String registrationId) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + return getRegistration(j, registrationId); + } + } + + @Override + public Registration getRegistrationByEndpoint(String endpoint) { + Validate.notNull(endpoint); + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] data = j.get(toEndpointKey(endpoint)); + if (data == null) { + return null; + } + return deserializeReg(data); + } + } + + @Override + public Registration getRegistrationByAdress(InetSocketAddress address) { + Validate.notNull(address); + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] ep = j.get(toRegAddrKey(address)); + if (ep == null) { + return null; + } + byte[] data = j.get(toEndpointKey(ep)); + if (data == null) { + return null; + } + return deserializeReg(data); + } + } + + @Override + public Iterator getAllRegistrations() { + return new TbLwM2mRedisRegistrationStore.RedisIterator(connectionFactory, new ScanParams().match(REG_EP + "*").count(100)); + } + + protected class RedisIterator implements Iterator { + + private final RedisConnectionFactory connectionFactory; + private final ScanParams scanParams; + + private String cursor; + private List scanResult; + + public RedisIterator(RedisConnectionFactory connectionFactory, ScanParams scanParams) { + this.connectionFactory = connectionFactory; + this.scanParams = scanParams; + // init scan result + scanNext("0"); + } + + private void scanNext(String cursor) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + do { + ScanResult sr = j.scan(cursor.getBytes(), scanParams); + + this.scanResult = new ArrayList<>(); + if (sr.getResult() != null && !sr.getResult().isEmpty()) { + for (byte[] value : j.mget(sr.getResult().toArray(new byte[][]{}))) { + this.scanResult.add(deserializeReg(value)); + } + } + + cursor = sr.getCursor(); + } while (!"0".equals(cursor) && scanResult.isEmpty()); + + this.cursor = cursor; + } + } + + @Override + public boolean hasNext() { + if (!scanResult.isEmpty()) { + return true; + } + if ("0".equals(cursor)) { + // no more elements to scan + return false; + } + + // read more elements + scanNext(cursor); + return !scanResult.isEmpty(); + } + + @Override + public Registration next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return scanResult.remove(0); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Deregistration removeRegistration(String registrationId) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + return removeRegistration(j, registrationId, false); + } + } + + private Deregistration removeRegistration(Jedis j, String registrationId, boolean removeOnlyIfNotAlive) { + // fetch the client ep by registration ID index + byte[] ep = j.get(toRegIdKey(registrationId)); + if (ep == null) { + return null; + } + + byte[] lockValue = null; + byte[] lockKey = toLockKey(ep); + try { + lockValue = lock.acquire(j, lockKey); + + // fetch the client + byte[] data = j.get(toEndpointKey(ep)); + if (data == null) { + return null; + } + Registration r = deserializeReg(data); + + if (!removeOnlyIfNotAlive || !r.isAlive(gracePeriod)) { + long nbRemoved = j.del(toRegIdKey(r.getId())); + if (nbRemoved > 0) { + j.del(toEndpointKey(r.getEndpoint())); + Collection obsRemoved = unsafeRemoveAllObservations(j, r.getId()); + removeAddrIndex(j, r); + removeExpiration(j, r); + return new Deregistration(r, obsRemoved); + } + } + return null; + } finally { + lock.release(j, lockKey, lockValue); + } + } + + private void removeAddrIndex(Jedis j, Registration registration) { + // Watch the key to remove. + byte[] regAddrKey = toRegAddrKey(registration.getSocketAddress()); + j.watch(regAddrKey); + + byte[] epFromAddr = j.get(regAddrKey); + // Delete the key if needed. + if (Arrays.equals(epFromAddr, registration.getEndpoint().getBytes(UTF_8))) { + // Try to delete the key + Transaction transaction = j.multi(); + transaction.del(regAddrKey); + transaction.exec(); + // if transaction failed this is not an issue as the socket address is probably reused and we don't neeed to + // delete it anymore. + } else { + // the key must not be deleted. + j.unwatch(); + } + } + + private void addOrUpdateExpiration(Jedis j, Registration registration) { + j.zadd(EXP_EP, registration.getExpirationTimeStamp(gracePeriod), registration.getEndpoint().getBytes(UTF_8)); + } + + private void removeExpiration(Jedis j, Registration registration) { + j.zrem(EXP_EP, registration.getEndpoint().getBytes(UTF_8)); + } + + private byte[] toRegIdKey(String registrationId) { + return toKey(REG_EP_REGID_IDX, registrationId); + } + + private byte[] toRegAddrKey(InetSocketAddress addr) { + return toKey(REG_EP_ADDR_IDX, addr.getAddress().toString() + ":" + addr.getPort()); + } + + private byte[] toEndpointKey(String endpoint) { + return toKey(REG_EP, endpoint); + } + + private byte[] toEndpointKey(byte[] endpoint) { + return toKey(REG_EP.getBytes(UTF_8), endpoint); + } + + private byte[] serializeReg(Registration registration) { + return RegistrationSerDes.bSerialize(registration); + } + + private Registration deserializeReg(byte[] data) { + return RegistrationSerDes.deserialize(data); + } + + /* *************** Leshan Observation API **************** */ + + /* + * The observation is not persisted here, it is done by the Californium layer (in the implementation of the + * org.eclipse.californium.core.observe.ObservationStore#add method) + */ + @Override + public Collection addObservation(String registrationId, Observation observation) { + + List removed = new ArrayList<>(); + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + + // fetch the client ep by registration ID index + byte[] ep = j.get(toRegIdKey(registrationId)); + if (ep == null) { + return null; + } + + byte[] lockValue = null; + byte[] lockKey = toLockKey(ep); + + try { + lockValue = lock.acquire(j, lockKey); + + // cancel existing observations for the same path and registration id. + for (Observation obs : getObservations(j, registrationId)) { + if (observation.getPath().equals(obs.getPath()) + && !Arrays.equals(observation.getId(), obs.getId())) { + removed.add(obs); + unsafeRemoveObservation(j, registrationId, obs.getId()); + } + } + + } finally { + lock.release(j, lockKey, lockValue); + } + } + return removed; + } + + @Override + public Observation removeObservation(String registrationId, byte[] observationId) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + + // fetch the client ep by registration ID index + byte[] ep = j.get(toRegIdKey(registrationId)); + if (ep == null) { + return null; + } + + // remove observation + byte[] lockValue = null; + byte[] lockKey = toLockKey(ep); + try { + lockValue = lock.acquire(j, lockKey); + + Observation observation = build(get(new Token(observationId))); + if (observation != null && registrationId.equals(observation.getRegistrationId())) { + unsafeRemoveObservation(j, registrationId, observationId); + return observation; + } + return null; + + } finally { + lock.release(j, lockKey, lockValue); + } + } + } + + @Override + public Observation getObservation(String registrationId, byte[] observationId) { + return build(get(new Token(observationId))); + } + + @Override + public Collection getObservations(String registrationId) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + return getObservations(j, registrationId); + } + } + + private Collection getObservations(Jedis j, String registrationId) { + Collection result = new ArrayList<>(); + for (byte[] token : j.lrange(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, -1)) { + byte[] obs = j.get(toKey(OBS_TKN, token)); + if (obs != null) { + result.add(build(deserializeObs(obs))); + } + } + return result; + } + + @Override + public Collection removeObservations(String registrationId) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + // check registration exists + Registration registration = getRegistration(j, registrationId); + if (registration == null) + return Collections.emptyList(); + + // get endpoint and create lock + String endpoint = registration.getEndpoint(); + byte[] lockValue = null; + byte[] lockKey = toKey(LOCK_EP, endpoint); + try { + lockValue = lock.acquire(j, lockKey); + + return unsafeRemoveAllObservations(j, registrationId); + } finally { + lock.release(j, lockKey, lockValue); + } + } + } + + /* *************** Californium ObservationStore API **************** */ + + @Override + public org.eclipse.californium.core.observe.Observation putIfAbsent(Token token, + org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { + return add(obs, true); + } + + @Override + public org.eclipse.californium.core.observe.Observation put(Token token, + org.eclipse.californium.core.observe.Observation obs) throws ObservationStoreException { + return add(obs, false); + } + + private org.eclipse.californium.core.observe.Observation add(org.eclipse.californium.core.observe.Observation obs, boolean ifAbsent) throws ObservationStoreException { + String endpoint = ObserveUtil.validateCoapObservation(obs); + org.eclipse.californium.core.observe.Observation previousObservation = null; + + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] lockValue = null; + byte[] lockKey = toKey(LOCK_EP, endpoint); + try { + lockValue = lock.acquire(j, lockKey); + + String registrationId = ObserveUtil.extractRegistrationId(obs); + if (!j.exists(toRegIdKey(registrationId))) + throw new ObservationStoreException("no registration for this Id"); + byte[] key = toKey(OBS_TKN, obs.getRequest().getToken().getBytes()); + byte[] serializeObs = serializeObs(obs); + byte[] previousValue; + if (ifAbsent) { + previousValue = j.get(key); + if (previousValue == null || previousValue.length == 0) { + j.set(key, serializeObs); + } else { + return deserializeObs(previousValue); + } + } else { + previousValue = j.getSet(key, serializeObs); + } + + // secondary index to get the list by registrationId + j.lpush(toKey(OBS_TKNS_REGID_IDX, registrationId), obs.getRequest().getToken().getBytes()); + + // log any collisions + if (previousValue != null && previousValue.length != 0) { + previousObservation = deserializeObs(previousValue); + LOG.warn( + "Token collision ? observation from request [{}] will be replaced by observation from request [{}] ", + previousObservation.getRequest(), obs.getRequest()); + } + } finally { + lock.release(j, lockKey, lockValue); + } + } + return previousObservation; + } + + @Override + public void remove(Token token) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] tokenKey = toKey(OBS_TKN, token.getBytes()); + + // fetch the observation by token + byte[] serializedObs = j.get(tokenKey); + if (serializedObs == null) + return; + + org.eclipse.californium.core.observe.Observation obs = deserializeObs(serializedObs); + String registrationId = ObserveUtil.extractRegistrationId(obs); + Registration registration = getRegistration(j, registrationId); + if (registration == null) { + LOG.warn("Unable to remove observation {}, registration {} does not exist anymore", obs.getRequest(), + registrationId); + return; + } + + String endpoint = registration.getEndpoint(); + byte[] lockValue = null; + byte[] lockKey = toKey(LOCK_EP, endpoint); + try { + lockValue = lock.acquire(j, lockKey); + + unsafeRemoveObservation(j, registrationId, token.getBytes()); + } finally { + lock.release(j, lockKey, lockValue); + } + } + + } + + @Override + public org.eclipse.californium.core.observe.Observation get(Token token) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] obs = j.get(toKey(OBS_TKN, token.getBytes())); + if (obs == null) { + return null; + } else { + return deserializeObs(obs); + } + } + } + + /* *************** Observation utility functions **************** */ + + private Registration getRegistration(Jedis j, String registrationId) { + byte[] ep = j.get(toRegIdKey(registrationId)); + if (ep == null) { + return null; + } + byte[] data = j.get(toEndpointKey(ep)); + if (data == null) { + return null; + } + + return deserializeReg(data); + } + + private void unsafeRemoveObservation(Jedis j, String registrationId, byte[] observationId) { + if (j.del(toKey(OBS_TKN, observationId)) > 0L) { + j.lrem(toKey(OBS_TKNS_REGID_IDX, registrationId), 0, observationId); + } + } + + private Collection unsafeRemoveAllObservations(Jedis j, String registrationId) { + Collection removed = new ArrayList<>(); + byte[] regIdKey = toKey(OBS_TKNS_REGID_IDX, registrationId); + + // fetch all observations by token + for (byte[] token : j.lrange(regIdKey, 0, -1)) { + byte[] obs = j.get(toKey(OBS_TKN, token)); + if (obs != null) { + removed.add(build(deserializeObs(obs))); + } + j.del(toKey(OBS_TKN, token)); + } + j.del(regIdKey); + + return removed; + } + + @Override + public void setContext(Token token, EndpointContext correlationContext) { + // In Leshan we always set context when we send the request, so this should not be needed to implement this. + } + + private byte[] serializeObs(org.eclipse.californium.core.observe.Observation obs) { + return ObservationSerDes.serialize(obs); + } + + private org.eclipse.californium.core.observe.Observation deserializeObs(byte[] data) { + return ObservationSerDes.deserialize(data); + } + + private Observation build(org.eclipse.californium.core.observe.Observation cfObs) { + if (cfObs == null) + return null; + + return ObserveUtil.createLwM2mObservation(cfObs.getRequest()); + } + + /* *************** Expiration handling **************** */ + + /** + * Start regular cleanup of dead registrations. + */ + @Override + public synchronized void start() { + if (!started) { + started = true; + cleanerTask = schedExecutor.scheduleAtFixedRate(new TbLwM2mRedisRegistrationStore.Cleaner(), cleanPeriod, cleanPeriod, TimeUnit.SECONDS); + } + } + + /** + * Stop the underlying cleanup of the registrations. + */ + @Override + public synchronized void stop() { + if (started) { + started = false; + if (cleanerTask != null) { + cleanerTask.cancel(false); + cleanerTask = null; + } + } + } + + /** + * Destroy "cleanup" scheduler. + */ + @Override + public synchronized void destroy() { + started = false; + schedExecutor.shutdownNow(); + try { + schedExecutor.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOG.warn("Destroying RedisRegistrationStore was interrupted.", e); + } + } + + private class Cleaner implements Runnable { + + @Override + public void run() { + + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + Set endpointsExpired = j.zrangeByScore(EXP_EP, Double.NEGATIVE_INFINITY, + System.currentTimeMillis(), 0, cleanLimit); + + for (byte[] endpoint : endpointsExpired) { + Registration r = deserializeReg(j.get(toEndpointKey(endpoint))); + if (!r.isAlive(gracePeriod)) { + Deregistration dereg = removeRegistration(j, r.getId(), true); + if (dereg != null) + expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); + } + } + } catch (Exception e) { + LOG.warn("Unexpected Exception while registration cleaning", e); + } + } + } + + @Override + public void setExpirationListener(ExpirationListener listener) { + expirationListener = listener; + } + + @Override + public void setExecutor(ScheduledExecutorService executor) { + // TODO should we reuse californium executor ? + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java new file mode 100644 index 0000000000..7d05630ba4 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java @@ -0,0 +1,148 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.store; + +import org.eclipse.leshan.server.redis.serialization.SecurityInfoSerDes; +import org.eclipse.leshan.server.security.EditableSecurityStore; +import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.eclipse.leshan.server.security.SecurityStoreListener; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.ScanParams; +import redis.clients.jedis.ScanResult; + +import java.util.Collection; +import java.util.LinkedList; + +@Service +public class TbLwM2mRedisSecurityStore implements EditableSecurityStore { + private static final String SEC_EP = "SEC#EP#"; + + private static final String PSKID_SEC = "PSKID#SEC"; + + private final RedisConnectionFactory connectionFactory; + private SecurityStoreListener listener; + + public TbLwM2mRedisSecurityStore(RedisConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + @Override + public SecurityInfo getByEndpoint(String endpoint) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] data = j.get((SEC_EP + endpoint).getBytes()); + if (data == null) { + return null; + } else { + return deserialize(data); + } + } + } + + @Override + public SecurityInfo getByIdentity(String identity) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + String ep = j.hget(PSKID_SEC, identity); + if (ep == null) { + return null; + } else { + byte[] data = j.get((SEC_EP + ep).getBytes()); + if (data == null) { + return null; + } else { + return deserialize(data); + } + } + } + } + + @Override + public Collection getAll() { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + ScanParams params = new ScanParams().match(SEC_EP + "*").count(100); + Collection list = new LinkedList<>(); + String cursor = "0"; + do { + ScanResult res = j.scan(cursor.getBytes(), params); + for (byte[] key : res.getResult()) { + byte[] element = j.get(key); + list.add(deserialize(element)); + } + cursor = res.getCursor(); + } while (!"0".equals(cursor)); + return list; + } + } + + @Override + public SecurityInfo add(SecurityInfo info) throws NonUniqueSecurityInfoException { + byte[] data = serialize(info); + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + if (info.getIdentity() != null) { + // populate the secondary index (security info by PSK id) + String oldEndpoint = j.hget(PSKID_SEC, info.getIdentity()); + if (oldEndpoint != null && !oldEndpoint.equals(info.getEndpoint())) { + throw new NonUniqueSecurityInfoException("PSK Identity " + info.getIdentity() + " is already used"); + } + j.hset(PSKID_SEC.getBytes(), info.getIdentity().getBytes(), info.getEndpoint().getBytes()); + } + + byte[] previousData = j.getSet((SEC_EP + info.getEndpoint()).getBytes(), data); + SecurityInfo previous = previousData == null ? null : deserialize(previousData); + String previousIdentity = previous == null ? null : previous.getIdentity(); + if (previousIdentity != null && !previousIdentity.equals(info.getIdentity())) { + j.hdel(PSKID_SEC, previousIdentity); + } + + return previous; + } + } + + @Override + public SecurityInfo remove(String endpoint, boolean infosAreCompromised) { + try (Jedis j = (Jedis) connectionFactory.getConnection().getNativeConnection()) { + byte[] data = j.get((SEC_EP + endpoint).getBytes()); + + if (data != null) { + SecurityInfo info = deserialize(data); + if (info.getIdentity() != null) { + j.hdel(PSKID_SEC.getBytes(), info.getIdentity().getBytes()); + } + j.del((SEC_EP + endpoint).getBytes()); + if (listener != null) { + listener.securityInfoRemoved(infosAreCompromised, info); + } + return info; + } + } + return null; + } + + private byte[] serialize(SecurityInfo secInfo) { + return SecurityInfoSerDes.serialize(secInfo); + } + + private SecurityInfo deserialize(byte[] data) { + return SecurityInfoSerDes.deserialize(data); + } + + @Override + public void setListener(SecurityStoreListener listener) { + this.listener = listener; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreConfiguration.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreConfiguration.java new file mode 100644 index 0000000000..9e4b7e442a --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mStoreConfiguration.java @@ -0,0 +1,123 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.store; + +import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.eclipse.leshan.server.californium.registration.InMemoryRegistrationStore; +import org.eclipse.leshan.server.security.EditableSecurityStore; +import org.eclipse.leshan.server.security.InMemorySecurityStore; +import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; +import org.eclipse.leshan.server.security.SecurityInfo; +import org.eclipse.leshan.server.security.SecurityStoreListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; + +import java.util.Collection; +import java.util.Optional; + +@Service +@TbLwM2mTransportComponent +public class TbLwM2mStoreConfiguration { + + @Autowired(required = false) + private Optional redisConfiguration; + + @Autowired + @Lazy + private LwM2mClientContext clientContext; + + @Value("${transport.lwm2m.redis.enabled:false}") + private boolean useRedis; + + @Bean + private CaliforniumRegistrationStore registrationStore() { + return redisConfiguration.isPresent() && useRedis ? + new TbLwM2mRedisRegistrationStore(redisConfiguration.get().redisConnectionFactory()) : new InMemoryRegistrationStore(); + } + + @Bean + private EditableSecurityStore securityStore() { + return new TbLwM2mSecurityStoreWrapper(redisConfiguration.isPresent() && useRedis ? + new TbLwM2mRedisSecurityStore(redisConfiguration.get().redisConnectionFactory()) : new InMemorySecurityStore()); + } + + public class TbLwM2mSecurityStoreWrapper implements EditableSecurityStore { + + private final EditableSecurityStore securityStore; + + public TbLwM2mSecurityStoreWrapper(EditableSecurityStore securityStore) { + this.securityStore = securityStore; + } + + @Override + public Collection getAll() { + return securityStore.getAll(); + } + + @Override + public SecurityInfo add(SecurityInfo info) throws NonUniqueSecurityInfoException { + return securityStore.add(info); + } + + @Override + public SecurityInfo remove(String endpoint, boolean infosAreCompromised) { + return securityStore.remove(endpoint, infosAreCompromised); + } + + @Override + public void setListener(SecurityStoreListener listener) { + securityStore.setListener(listener); + } + + @Override + public SecurityInfo getByEndpoint(String endpoint) { + SecurityInfo securityInfo = securityStore.getByEndpoint(endpoint); + if (securityInfo == null) { + securityInfo = clientContext.addLwM2mClientToSession(endpoint).getSecurityInfo(); + try { + if (securityInfo != null) { + add(securityInfo); + } + } catch (NonUniqueSecurityInfoException e) { + e.printStackTrace(); + } + } + return securityInfo; + } + + @Override + public SecurityInfo getByIdentity(String pskIdentity) { + SecurityInfo securityInfo = securityStore.getByIdentity(pskIdentity); + if (securityInfo == null) { + securityInfo = clientContext.addLwM2mClientToSession(pskIdentity).getSecurityInfo(); + try { + if (securityInfo != null) { + add(securityInfo); + } + } catch (NonUniqueSecurityInfoException e) { + e.printStackTrace(); + } + } + return securityInfo; + } + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java new file mode 100644 index 0000000000..67ce459d9b --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -0,0 +1,159 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.utils; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ResourceModel.Type; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.core.node.codec.CodecException; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; +import org.eclipse.leshan.core.util.Hex; +import org.eclipse.leshan.core.util.StringUtils; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +@Slf4j +public class LwM2mValueConverterImpl implements LwM2mValueConverter { + + private static final LwM2mValueConverterImpl INSTANCE = new LwM2mValueConverterImpl(); + + public static LwM2mValueConverterImpl getInstance() { + return INSTANCE; + } + + @Override + public Object convertValue(Object value, Type currentType, Type expectedType, LwM2mPath resourcePath) + throws CodecException { + if (expectedType == null) { + /** unknown resource, trusted value */ + return value; + } + + if (currentType == expectedType) { + /** expected type */ + return value; + } + + switch (expectedType) { + case INTEGER: + switch (currentType) { + case FLOAT: + log.debug("Trying to convert float value [{}] to integer", value); + Long longValue = ((Double) value).longValue(); + if ((double) value == longValue.doubleValue()) { + return longValue; + } + default: + break; + } + break; + case FLOAT: + switch (currentType) { + case INTEGER: + log.debug("Trying to convert integer value [{}] to float", value); + Double floatValue = ((Long) value).doubleValue(); + if ((long) value == floatValue.longValue()) { + return floatValue; + } + default: + break; + } + break; + case BOOLEAN: + switch (currentType) { + case STRING: + log.debug("Trying to convert string value {} to boolean", value); + if (StringUtils.equalsIgnoreCase((String) value, "true")) { + return true; + } else if (StringUtils.equalsIgnoreCase((String) value, "false")) { + return false; + } + break; + case INTEGER: + log.debug("Trying to convert int value {} to boolean", value); + Long val = (Long) value; + if (val == 1) { + return true; + } else if (val == 0) { + return false; + } + break; + default: + break; + } + break; + case TIME: + switch (currentType) { + case INTEGER: + log.debug("Trying to convert long value {} to date", value); + /** let's assume we received the millisecond since 1970/1/1 */ + return new Date((Long) value); + case STRING: + log.debug("Trying to convert string value {} to date", value); + /** let's assume we received an ISO 8601 format date */ + try { + DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); + XMLGregorianCalendar cal = datatypeFactory.newXMLGregorianCalendar((String) value); + return cal.toGregorianCalendar().getTime(); + } catch (DatatypeConfigurationException | IllegalArgumentException e) { + log.debug("Unable to convert string to date", e); + throw new CodecException("Unable to convert string (%s) to date for resource %s", value, + resourcePath); + } + default: + break; + } + break; + case STRING: + switch (currentType) { + case BOOLEAN: + case INTEGER: + case FLOAT: + return String.valueOf(value); + case TIME: + String DATE_FORMAT = "MMM d, yyyy HH:mm a"; + Long timeValue = ((Date) value).getTime(); + DateFormat formatter = new SimpleDateFormat(DATE_FORMAT); + return formatter.format(new Date(timeValue)); + default: + break; + } + break; + case OPAQUE: + if (currentType == Type.STRING) { + /** let's assume we received an hexadecimal string */ + log.debug("Trying to convert hexadecimal string [{}] to byte array", value); + // TODO check if we shouldn't instead assume that the string contains Base64 encoded data + try { + return Hex.decodeHex(((String) value).toCharArray()); + } catch (IllegalArgumentException e) { + throw new CodecException("Unable to convert hexastring [%s] to byte array for resource %s", value, + resourcePath); + } + } + break; + default: + } + + throw new CodecException("Invalid value type for resource %s, expected %s, got %s", resourcePath, expectedType, + currentType); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/TypeServer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/TypeServer.java new file mode 100644 index 0000000000..732d761e68 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/TypeServer.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.utils; + +public enum TypeServer { + BOOTSTRAP(0, "bootstrap"), + CLIENT(1, "client"); + + public int code; + public String type; + + TypeServer(int code, String type) { + this.code = code; + this.type = type; + } +} diff --git a/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks b/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks new file mode 100644 index 0000000000..9f6748f8fd Binary files /dev/null and b/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks differ diff --git a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh new file mode 100755 index 0000000000..dcf38b4f10 --- /dev/null +++ b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh @@ -0,0 +1,360 @@ +#!/bin/sh +# +# Copyright © 2016-2021 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. +# + +#/home/nick/Igor_project/Thingsboard_Perfrmance_test/performance-tests/src/main/resources/credentials/shell/lwM2M_credentials.sh -p LwX509 -s 0 -f 2000 -a client_alias_ -e client_self_signed_ -b bootstrap -d server -j serverKeyStore.jks -k clientKeyStore.jks -c client_ks_password -w server_ks_password + +#p) CLIENT_CN=$CLIENT_PREFIX00000000 +#s) client_start=0 +#f) client_finish=1 +#a) CLIENT_ALIAS=CLIENT_ALIAS_PREFIX_00000000 +#e) CLIENT_SELF_ALIAS=CLIENT_SELF_ALIAS_PREFIX_00000000 +#b) BOOTSTRAP_ALIAS=bootstrap +#d) SERVER_ALIAS=server +#j) SERVER_STORE=serverKeyStore.jks +#k) CLIENT_STORE=clientKeyStore.jks +#c) CLIENT_STORE_PWD=client_ks_password +#w) SERVER_STORE_PWD=server_ks_password +#l) ROOT_KEY_ALIAS=root_key_alias + +while getopts p:s:f:a:e:b:d:j:k:c:w:l: flag; do + case "${flag}" in + p) client_pref=${OPTARG} ;; + s) client_start=${OPTARG} ;; + f) client_finish=${OPTARG} ;; + a) client_alias_pref=${OPTARG} ;; + e) client_self_alias_pref=${OPTARG} ;; + b) bootstrap_alias=${OPTARG} ;; + d) server_alias=${OPTARG} ;; + j) key_store_server_file=${OPTARG} ;; + k) key_store_client_file=${OPTARG} ;; + c) client_key_store_pwd=${OPTARG} ;; + w) server_key_store_pwd=${OPTARG} ;; + w) root_key_alias=${OPTARG} ;; + esac +done + +# cd to dir of script +script_dir=$(dirname $0) +echo "script_dir: $script_dir" +cd $script_dir +# source the properties: +. ./lwM2M_keygen.properties + +if [ -n "$client_pref" ]; then + CLIENT_PREFIX=$client_pref +fi + +if [ -z "$client_start" ]; then + client_start=0 +fi + +if [ -z "$client_finish" ]; then + client_finish=1 +fi + +if [ -n "$client_alias_pref" ]; then + CLIENT_ALIAS_PREFIX=$client_alias_pref +fi + +if [ -n "$client_self_alias_pref" ]; then + CLIENT_SELF_ALIAS_PREFIX=$client_self_alias_pref +fi + +if [ -n "$bootstrap_alias" ]; then + BOOTSTRAP_ALIAS=$bootstrap_alias +fi + +if [ -n "$server_alias" ]; then + SERVER_ALIAS=$server_alias +fi + +if [ -n "$key_store_server_file" ]; then + SERVER_STORE=$key_store_server_file +fi + +if [ -n "$key_store_client_file" ]; then + CLIENT_STORE=$key_store_client_file +fi + +if [ -n "$client_key_store_pwd" ]; then + CLIENT_STORE_PWD=$client_key_store_pwd +fi + +if [ -n "$server_key_store_pwd" ]; then + SERVER_STORE_PWD=$server_key_store_pwd +fi + +if [ -n "$root_key_alias" ]; then + ROOT_KEY_ALIAS=$root_key_alias +fi + +CLIENT_NUMBER=$client_start + +echo "==Start==" +echo "CLIENT_PREFIX: $CLIENT_PREFIX" +echo "client_start: $client_start" +echo "client_finish: $client_finish" +echo "CLIENT_ALIAS_PREFIX: $CLIENT_ALIAS_PREFIX" +echo "CLIENT_SELF_ALIAS_PREFIX: $CLIENT_SELF_ALIAS_PREFIX" +echo "BOOTSTRAP_ALIAS: $BOOTSTRAP_ALIAS" +echo "SERVER_ALIAS: $SERVER_ALIAS" +echo "SERVER_STORE: $SERVER_STORE" +echo "CLIENT_STORE: $CLIENT_STORE" +echo "CLIENT_STORE_PWD: $CLIENT_STORE_PWD" +echo "SERVER_STORE_PWD: $SERVER_STORE_PWD" +echo "CLIENT_NUMBER: $CLIENT_NUMBER" +echo "ROOT_KEY_ALIAS: $ROOT_KEY_ALIAS" + +end_point() { + echo "$CLIENT_PREFIX$(printf "%08d" $CLIENT_NUMBER)" +} + +client_alias_point() { + echo "$CLIENT_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" +} + +client_self_alias_point() { + echo "$CLIENT_SELF_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" +} + +# Generation of the keystore. +echo "${H0}====START========${RESET}" +echo "${H1}Server Keystore : ${RESET}" +echo "${H1}==================${RESET}" +echo "${H2}Creating the trusted root CA key and certificate...${RESET}" +# -keysize +# 1024 (when using -genkeypair) +keytool \ + -genkeypair \ + -alias $ROOT_KEY_ALIAS \ + -keyalg EC \ + -dname "CN=$ROOT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $SERVER_STORE_PWD \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD + +echo +echo "${H2}Creating server key and self-signed certificate ...${RESET}" +keytool \ + -genkeypair \ + -alias $SERVER_ALIAS \ + -keyalg EC \ + -dname "CN=$SERVER_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $SERVER_STORE_PWD \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD +keytool \ + -exportcert \ + -alias $SERVER_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -importcert \ + -alias $SERVER_SELF_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -noprompt + +echo +echo "${H2}Creating server certificate signed by root CA...${RESET}" +keytool \ + -certreq \ + -alias $SERVER_ALIAS \ + -dname "CN=$SERVER_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -gencert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -storetype $STORETYPE \ + -validity $VALIDITY | + keytool \ + -importcert \ + -alias $SERVER_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD + +echo +echo "${H2}Creating bootstrap key and self-signed certificate ...${RESET}" +keytool \ + -genkeypair \ + -alias $BOOTSTRAP_ALIAS \ + -keyalg EC \ + -dname "CN=$BOOTSTRAP_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $SERVER_STORE_PWD \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD +keytool \ + -exportcert \ + -alias $BOOTSTRAP_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -importcert \ + -alias $BOOTSTRAP_SELF_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -noprompt + +echo +echo "${H2}Creating bootstrap certificate signed by root CA...${RESET}" +keytool \ + -certreq \ + -alias $BOOTSTRAP_ALIAS \ + -dname "CN=$BOOTSTRAP_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -gencert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -storetype $STORETYPE \ + -validity $VALIDITY | + keytool \ + -importcert \ + -alias $BOOTSTRAP_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD + +if [ "$client_start" -lt "$client_finish" ]; then + echo + echo "${H2}Import root certificate just to be able to import need by root CA with expected CN to $CLIENT_STORE${RESET}" + keytool \ + -exportcert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -importcert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD \ + -noprompt +fi + +cert_end_point() { + echo + echo "${H1}Client Keystore : ${RESET}" + echo "${H1}==================${RESET}" + echo "${H2}Creating client key and self-signed certificate with expected CN CLIENT_ALIAS: $CLIENT_ALIAS${RESET}" + keytool \ + -genkeypair \ + -alias $CLIENT_ALIAS \ + -keyalg EC \ + -dname "CN=$CLIENT_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $CLIENT_STORE_PWD \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD + keytool \ + -exportcert \ + -alias $CLIENT_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD | + keytool \ + -importcert \ + -alias $CLIENT_SELF_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD \ + -noprompt +# +# echo +# echo "${H2}Import root certificate just to be able to import ned by root CA with expected CN...${RESET}" +# keytool \ +# -exportcert \ +# -alias $ROOT_KEY_ALIAS \ +# -keystore $SERVER_STORE \ +# -storepass $SERVER_STORE_PWD | +# keytool \ +# -importcert \ +# -alias $ROOT_KEY_ALIAS \ +# -keystore $CLIENT_STORE \ +# -storepass $CLIENT_STORE_PWD \ +# -noprompt +# + + echo + echo "${H2}Creating client certificate signed by root CA with expected CN CLIENT_ALIAS: $CLIENT_ALIAS CLIENT_CN: $CLIENT_CN${RESET}" + keytool \ + -certreq \ + -alias $CLIENT_ALIAS \ + -dname "CN=$CLIENT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD | + keytool \ + -gencert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -storetype $STORETYPE \ + -validity $VALIDITY | + keytool \ + -importcert \ + -alias $CLIENT_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD \ + -noprompt +} + +if [ "$client_start" -lt "$client_finish" ]; then + echo "Файл содержит, как минимум, одно слово Bash." + echo + echo "==Start Client==" + while [ "$CLIENT_NUMBER" -lt "$client_finish" ]; do + echo "number $CLIENT_NUMBER" + echo "finish $client_finish" + CLIENT_CN=$(end_point) + CLIENT_ALIAS=$(client_alias_point) + CLIENT_SELF_ALIAS=$(client_self_alias_point) + echo "CLIENT_CN $CLIENT_CN" + echo "CLIENT_ALIAS $CLIENT_ALIAS" + echo "CLIENT_SELF_ALIAS $CLIENT_SELF_ALIAS" + cert_end_point + CLIENT_NUMBER=$(($CLIENT_NUMBER + 1)) + echo + done +fi + +echo +echo "${H0}!!! Warning ${H2}Migrate ${H1}${SERVER_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" +keytool \ + -importkeystore \ + -srckeystore $SERVER_STORE \ + -destkeystore $SERVER_STORE \ + -deststoretype pkcs12 \ + -srcstorepass $SERVER_STORE_PWD + +if [ "$client_start" -lt "$client_finish" ]; then + echo + echo "${H0}!!! Warning ${H2}Migrate ${H1}${CLIENT_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" + keytool \ + -importkeystore \ + -srckeystore $CLIENT_STORE \ + -destkeystore $CLIENT_STORE \ + -deststoretype pkcs12 \ + -srcstorepass $CLIENT_STORE_PWD +fi diff --git a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties new file mode 100644 index 0000000000..7b3cd9c09a --- /dev/null +++ b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties @@ -0,0 +1,57 @@ +# +# Copyright © 2016-2017 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. +# + +# Keystore common parameters +ROOT_KEY_ALIAS=rootCA +DOMAIN_SUFFIX="$(hostname)" +ROOT_CN="$DOMAIN_SUFFIX $ROOT_KEY_ALIAS" +ORGANIZATIONAL_UNIT=Thingsboard +ORGANIZATION=Thingsboard +CITY=SF +STATE_OR_PROVINCE=CA +TWO_LETTER_COUNTRY_CODE=US +VALIDITY=36500 #days +STORETYPE="JKS" + +#Server +SERVER_STORE=serverKeyStore1.jks +SERVER_STORE_PWD=server_ks_password1 +SERVER_ALIAS=server1 +SERVER_CN="$DOMAIN_SUFFIX server LwM2M signed by root CA" +SERVER_SELF_ALIAS=server_self_signed +SERVER_SELF_CN="$DOMAIN_SUFFIX server LwM2M self-signed" +BOOTSTRAP_ALIAS=bootstrap1 +BOOTSTRAP_CN="$DOMAIN_SUFFIX bootstrap server LwM2M signed by root CA" +BOOTSTRAP_SELF_ALIAS=bootstrap_self_signed +BOOTSTRAP_SELF_CN="$DOMAIN_SUFFIX bootstrap server LwM2M self-signed" + +# Client +CLIENT_STORE=clientKeyStore1.jks +CLIENT_STORE_PWD=client_ks_password1 +CLIENT_ALIAS_PREFIX=client_alias_1 +CLIENT_PREFIX=LwX509___ +CLIENT_SELF_ALIAS_PREFIX=client_self_signed_1 +CLIENT_SELF_CN="$DOMAIN_SUFFIX client LwM2M self-signed" + +# Color output stuff +red=`tput setaf 1` +green=`tput setaf 2` +blue=`tput setaf 4` +bold=`tput bold` +H0=${red}${bold} +H1=${green}${bold} +H2=${blue} +RESET=`tput sgr0` diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 0d231581b9..676593804e 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.common.transport @@ -90,7 +90,7 @@ org.mockito - mockito-all + mockito-core test 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 ec4247b718..c2cf3686e9 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 @@ -45,6 +45,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.security.KeyStore; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.concurrent.CountDownLatch; @@ -154,7 +155,7 @@ public class MqttSslHandlerProvider { String credentialsBody = null; for (X509Certificate cert : chain) { try { - String strCert = SslUtil.getX509CertificateString(cert); + String strCert = SslUtil.getCertificateString(cert); String sha3Hash = EncryptionUtil.getSha3Hash(strCert); final String[] credentialsBodyHolder = new String[1]; CountDownLatch latch = new CountDownLatch(1); @@ -179,7 +180,7 @@ public class MqttSslHandlerProvider { credentialsBody = credentialsBodyHolder[0]; break; } - } catch (InterruptedException | IOException e) { + } catch (InterruptedException | CertificateEncodingException e) { log.error(e.getMessage(), e); } } 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 e7c295b9e6..06a8dcacdc 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 @@ -35,6 +35,7 @@ import io.netty.handler.codec.mqtt.MqttSubscribeMessage; import io.netty.handler.codec.mqtt.MqttTopicSubscription; import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; import io.netty.handler.ssl.SslHandler; +import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; @@ -55,6 +56,7 @@ import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; import org.thingsboard.server.common.transport.service.DefaultTransportService; +import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionEvent; @@ -67,7 +69,8 @@ import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; import org.thingsboard.server.transport.mqtt.util.SslUtil; import javax.net.ssl.SSLPeerUnverifiedException; -import javax.security.cert.X509Certificate; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.io.IOException; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -314,7 +317,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final int msgId, final T msg) { - return new TransportServiceCallback() { + return new TransportServiceCallback<>() { @Override public void onSuccess(Void dummy) { log.trace("[{}] Published msg: {}", sessionId, msg); @@ -481,12 +484,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (userName != null) { request.setUserName(userName); } - String password = connectMessage.payload().password(); - if (password != null) { + byte[] passwordBytes = connectMessage.payload().passwordInBytes(); + if (passwordBytes != null) { + String password = new String(passwordBytes, CharsetUtil.UTF_8); request.setPassword(password); } transportService.process(DeviceTransportType.MQTT, request.build(), - new TransportServiceCallback() { + new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { onValidateDeviceResponse(msg, ctx, connectMessage); @@ -506,10 +510,10 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (!context.isSkipValidityCheckForClientCert()) { cert.checkValidity(); } - String strCert = SslUtil.getX509CertificateString(cert); + String strCert = SslUtil.getCertificateString(cert); String sha3Hash = EncryptionUtil.getSha3Hash(strCert); transportService.process(DeviceTransportType.MQTT, ValidateDeviceX509CertRequestMsg.newBuilder().setHash(sha3Hash).build(), - new TransportServiceCallback() { + new TransportServiceCallback<>() { @Override public void onSuccess(ValidateDeviceCredentialsResponse msg) { onValidateDeviceResponse(msg, ctx, connectMessage); @@ -530,9 +534,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private X509Certificate getX509Certificate() { try { - X509Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificateChain(); + Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificates(); if (certChain.length > 0) { - return certChain[0]; + return (X509Certificate) certChain[0]; } } catch (SSLPeerUnverifiedException e) { log.warn(e.getMessage()); @@ -596,7 +600,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } - private void checkGatewaySession() { + private void checkGatewaySession(SessionMetaData sessionMetaData) { TransportDeviceInfo device = deviceSessionCtx.getDeviceInfo(); try { JsonNode infoNode = context.getMapper().readTree(device.getAdditionalInfo()); @@ -604,6 +608,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement JsonNode gatewayNode = infoNode.get("gateway"); if (gatewayNode != null && gatewayNode.asBoolean()) { gatewaySessionHandler = new GatewaySessionHandler(deviceSessionCtx, sessionId); + if (infoNode.has(DefaultTransportService.OVERWRITE_ACTIVITY_TIME) && infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).isBoolean()) { + sessionMetaData.setOverwriteActivityTime(infoNode.get(DefaultTransportService.OVERWRITE_ACTIVITY_TIME).asBoolean()); + } } } } catch (IOException e) { @@ -639,8 +646,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement transportService.process(deviceSessionCtx.getSessionInfo(), DefaultTransportService.getSessionEventMsg(SessionEvent.OPEN), new TransportServiceCallback() { @Override public void onSuccess(Void msg) { - transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); - checkGatewaySession(); + SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); + checkGatewaySession(sessionMetaData); ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage)); log.info("[{}] Client connected!", sessionId); } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java index 60a2503225..a7d5499cfb 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java @@ -102,4 +102,5 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { // This feature is not supported in the TB IoT Gateway yet. } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java index bd0d034c35..f376077b84 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/SslUtil.java @@ -20,8 +20,8 @@ import org.springframework.util.Base64Utils; import org.thingsboard.server.common.msg.EncryptionUtil; import java.io.IOException; +import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; /** * @author Valerii Sosliuk @@ -32,15 +32,8 @@ public class SslUtil { private SslUtil() { } - public static String getX509CertificateString(X509Certificate cert) - throws CertificateEncodingException, IOException { - Base64Utils.encodeToString(cert.getEncoded()); - return EncryptionUtil.trimNewLines(Base64Utils.encodeToString(cert.getEncoded())); - } - - public static String getX509CertificateString(javax.security.cert.X509Certificate cert) - throws javax.security.cert.CertificateEncodingException, IOException { - Base64Utils.encodeToString(cert.getEncoded()); + public static String getCertificateString(Certificate cert) + throws CertificateEncodingException { return EncryptionUtil.trimNewLines(Base64Utils.encodeToString(cert.getEncoded())); } } diff --git a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java index 178d174543..cc59d8bd0b 100644 --- a/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java +++ b/common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/util/MqttTopicFilterFactoryTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.transport.mqtt.util; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import javax.script.ScriptException; diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 35722cdd1d..65d96ae48d 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -39,6 +39,7 @@ mqtt http coap + lwm2m snmp diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 1f334b9699..c7e22ecc80 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 6c8fdafd84..491adc189e 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.common.transport @@ -52,6 +52,10 @@ org.thingsboard.common message + + org.thingsboard.common + cache + org.thingsboard.common util @@ -87,7 +91,7 @@ org.mockito - mockito-all + mockito-core test @@ -111,6 +115,15 @@ com.google.protobuf protobuf-java + + org.eclipse.leshan + leshan-core + compile + + + org.eclipse.leshan + leshan-server-cf + diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java index 97cfd8fc5e..8209fe2531 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java @@ -23,6 +23,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotif import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportUpdateCredentialsProto; import java.util.Optional; @@ -41,6 +42,8 @@ public interface SessionMsgListener { void onToServerRpcResponse(ToServerRpcResponseMsg toServerResponse); + default void onToTransportUpdateCredentials(ToTransportUpdateCredentialsProto toTransportUpdateCredentials){} + default void onDeviceProfileUpdate(TransportProtos.SessionInfoProto newSessionInfo, DeviceProfile deviceProfile) { } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java index 7ac1805621..75f20cc825 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java @@ -20,10 +20,10 @@ import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.scheduler.SchedulerComponent; +import org.thingsboard.server.queue.util.TbTransportComponent; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -35,16 +35,16 @@ import java.util.concurrent.Executors; */ @Slf4j @Data -@Service -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true')") public abstract class TransportContext { protected final ObjectMapper mapper = new ObjectMapper(); @Autowired private TransportService transportService; + @Autowired private TbServiceInfoProvider serviceInfoProvider; + @Autowired private SchedulerComponent scheduler; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index 6ec1337c62..ab728af00d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -19,11 +19,16 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.service.SessionMetaData; import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetEntityProfileResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetResourcesRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.GetResourcesResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.LwM2MRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.LwM2MResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostTelemetryMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; @@ -36,6 +41,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProt import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; @@ -46,6 +53,8 @@ public interface TransportService { GetEntityProfileResponseMsg getEntityProfile(GetEntityProfileRequestMsg msg); + GetResourcesResponseMsg getResources(GetResourcesRequestMsg msg); + void process(DeviceTransportType transportType, ValidateDeviceTokenRequestMsg msg, TransportServiceCallback callback); @@ -55,12 +64,20 @@ public interface TransportService { void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback); + void process(ValidateDeviceLwM2MCredentialsRequestMsg msg, + TransportServiceCallback callback); + void process(GetOrCreateDeviceFromGatewayRequestMsg msg, TransportServiceCallback callback); void process(ProvisionDeviceRequestMsg msg, TransportServiceCallback callback); + void onProfileUpdate(DeviceProfile deviceProfile); + + void process(LwM2MRequestMsg msg, + TransportServiceCallback callback); + void process(SessionInfoProto sessionInfo, SessionEventMsg msg, TransportServiceCallback callback); void process(SessionInfoProto sessionInfo, PostTelemetryMsg msg, TransportServiceCallback callback); @@ -81,11 +98,11 @@ public interface TransportService { void process(SessionInfoProto sessionInfo, ClaimDeviceMsg msg, TransportServiceCallback callback); - void registerAsyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener); + SessionMetaData registerAsyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener); - void registerSyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout); + SessionMetaData registerSyncSession(SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout); - void reportActivity(SessionInfoProto sessionInfo); + SessionMetaData reportActivity(SessionInfoProto sessionInfo); void deregisterSession(SessionInfoProto sessionInfo); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index 362be1ed00..2c5dfafe4e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -51,6 +51,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCre import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -59,7 +60,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; -import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -245,12 +245,25 @@ public class JsonConverter { } private static void parseNumericValue(List result, Entry valueEntry, JsonPrimitive value) { - if (value.getAsString().contains(".")) { - result.add(new DoubleDataEntry(valueEntry.getKey(), value.getAsDouble())); + String valueAsString = value.getAsString(); + String key = valueEntry.getKey(); + if (valueAsString.contains("e") || valueAsString.contains("E")) { + var bd = new BigDecimal(valueAsString); + if (bd.stripTrailingZeros().scale() <= 0) { + try { + result.add(new LongDataEntry(key, bd.longValueExact())); + } catch (ArithmeticException e) { + result.add(new DoubleDataEntry(key, bd.doubleValue())); + } + } else { + result.add(new DoubleDataEntry(key, bd.doubleValue())); + } + } else if (valueAsString.contains(".")) { + result.add(new DoubleDataEntry(key, value.getAsDouble())); } else { try { long longValue = Long.parseLong(value.getAsString()); - result.add(new LongDataEntry(valueEntry.getKey(), longValue)); + result.add(new LongDataEntry(key, longValue)); } catch (NumberFormatException e) { throw new JsonSyntaxException("Big integer values are not supported!"); } @@ -285,7 +298,8 @@ public class JsonConverter { return result; } - public static JsonObject getJsonObjectForGateway(String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) { + public static JsonObject getJsonObjectForGateway(String deviceName, TransportProtos.GetAttributeResponseMsg + responseMsg) { JsonObject result = new JsonObject(); result.addProperty("id", responseMsg.getRequestId()); result.addProperty(DEVICE_PROPERTY, deviceName); @@ -298,7 +312,8 @@ public class JsonConverter { return result; } - public static JsonObject getJsonObjectForGateway(String deviceName, AttributeUpdateNotificationMsg notificationMsg) { + public static JsonObject getJsonObjectForGateway(String deviceName, AttributeUpdateNotificationMsg + notificationMsg) { JsonObject result = new JsonObject(); result.addProperty(DEVICE_PROPERTY, deviceName); result.add("data", toJson(notificationMsg)); @@ -426,6 +441,8 @@ public class JsonConverter { case MQTT_BASIC: result.add("credentialsValue", JSON_PARSER.parse(payload.getCredentialsValue()).getAsJsonObject()); break; + case LWM2M_CREDENTIALS: + break; } result.addProperty("credentialsType", payload.getCredentialsType().name()); result.addProperty("status", ProvisionResponseStatus.SUCCESS.name()); @@ -446,7 +463,8 @@ public class JsonConverter { return result; } - public static JsonElement toGatewayJson(String deviceName, TransportProtos.ProvisionDeviceResponseMsg responseRequest) { + public static JsonElement toGatewayJson(String deviceName, TransportProtos.ProvisionDeviceResponseMsg + responseRequest) { JsonObject result = new JsonObject(); result.addProperty(DEVICE_PROPERTY, deviceName); result.add("data", JsonConverter.toJson(responseRequest)); @@ -496,15 +514,18 @@ public class JsonConverter { return result; } - public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs) throws JsonSyntaxException { + public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs) throws + JsonSyntaxException { return convertToTelemetry(jsonElement, systemTs, false); } - public static Map> convertToSortedTelemetry(JsonElement jsonElement, long systemTs) throws JsonSyntaxException { + public static Map> convertToSortedTelemetry(JsonElement jsonElement, long systemTs) throws + JsonSyntaxException { return convertToTelemetry(jsonElement, systemTs, true); } - public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs, boolean sorted) throws JsonSyntaxException { + public static Map> convertToTelemetry(JsonElement jsonElement, long systemTs, boolean sorted) throws + JsonSyntaxException { Map> result = sorted ? new TreeMap<>() : new HashMap<>(); convertToTelemetry(jsonElement, systemTs, result, null); return result; @@ -574,7 +595,8 @@ public class JsonConverter { .build(); } - private static TransportProtos.ProvisionDeviceCredentialsMsg buildProvisionDeviceCredentialsMsg(String provisionKey, String provisionSecret) { + private static TransportProtos.ProvisionDeviceCredentialsMsg buildProvisionDeviceCredentialsMsg(String + provisionKey, String provisionSecret) { return TransportProtos.ProvisionDeviceCredentialsMsg.newBuilder() .setProvisionDeviceKey(provisionKey) .setProvisionDeviceSecret(provisionSecret) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java index 235ae6b3a3..ab18b930f9 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/auth/SessionInfoCreator.java @@ -15,16 +15,17 @@ */ package org.thingsboard.server.common.transport.auth; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.transport.TransportContext; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.UUID; +@Slf4j public class SessionInfoCreator { public static TransportProtos.SessionInfoProto create(ValidateDeviceCredentialsResponse msg, TransportContext context, UUID sessionId) { - return TransportProtos.SessionInfoProto.newBuilder() - .setNodeId(context.getNodeId()) + return TransportProtos.SessionInfoProto.newBuilder().setNodeId(context.getNodeId()) .setSessionIdMSB(sessionId.getMostSignificantBits()) .setSessionIdLSB(sessionId.getLeastSignificantBits()) .setDeviceIdMSB(msg.getDeviceInfo().getDeviceId().getId().getMostSignificantBits()) diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigBootstrap.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigBootstrap.java new file mode 100644 index 0000000000..14da88ecd1 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigBootstrap.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; +import org.thingsboard.server.gen.transport.TransportProtos; + +import java.security.PublicKey; +import java.util.Map; + +@Slf4j +@Component +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") +public class LwM2MTransportConfigBootstrap { + + @Getter + @Value("${transport.lwm2m.bootstrap.enable:}") + private Boolean bootstrapEnable; + + @Getter + @Value("${transport.lwm2m.bootstrap.id:}") + private Integer bootstrapServerId; + + @Getter + @Value("${transport.lwm2m.bootstrap.bind_address:}") + private String bootstrapHost; + + @Getter + @Value("${transport.lwm2m.bootstrap.bind_port_no_sec:}") + private Integer bootstrapPortNoSec; + + @Getter + @Value("${transport.lwm2m.bootstrap.secure.bind_address_security:}") + private String bootstrapHostSecurity; + + @Getter + @Value("${transport.lwm2m.bootstrap.secure.bind_port_security:}") + private Integer bootstrapPortSecurity; + + @Getter + @Value("${transport.lwm2m.bootstrap.secure.public_x:}") + private String bootstrapPublicX; + + @Getter + @Value("${transport.lwm2m.bootstrap.secure.public_y:}") + private String bootstrapPublicY; + + @Getter + @Setter + private PublicKey bootstrapPublicKey; + + @Getter + @Value("${transport.lwm2m.bootstrap.secure.private_encoded:}") + private String bootstrapPrivateEncoded; + + @Getter + @Value("${transport.lwm2m.bootstrap.secure.alias:}") + private String bootstrapAlias; + + @Getter + @Setter + private Map sessions; +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java new file mode 100644 index 0000000000..810848a5ca --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java @@ -0,0 +1,273 @@ +/** + * Copyright © 2016-2021 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.transport.lwm2m; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.model.ObjectLoader; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.ResourceModel; +import org.eclipse.leshan.core.node.LwM2mPath; +import org.eclipse.leshan.server.registration.Registration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") +public class LwM2MTransportConfigServer { + + + @Getter + private String MODEL_PATH_DEFAULT = "models"; + + @Getter + private String KEY_STORE_DEFAULT_RESOURCE_PATH = "credentials"; + + @Getter + private String KEY_STORE_DEFAULT_FILE = "serverKeyStore.jks"; + + @Getter + private String APP_DIR = "common"; + + @Getter + private String TRANSPORT_DIR = "transport"; + + @Getter + private String LWM2M_DIR = "lwm2m"; + + @Getter + private String SRC_DIR = "src"; + + @Getter + private String MAIN_DIR = "main"; + + @Getter + private String RESOURCES_DIR = "resources"; + + @Getter + private String BASE_DIR_PATH = System.getProperty("user.dir"); + + @Getter + // private String PATH_DATA_MICROSERVICE = "/usr/share/tb-lwm2m-transport/data$"; + private String PATH_DATA = "data"; + + @Getter + @Setter + private List modelsValue; + + @Getter + @Value("${transport.lwm2m.timeout:}") + private Long timeout; + + @Getter + @Value("${transport.sessions.report_timeout}") + private long sessionReportTimeout; + + @Getter + @Value("${transport.lwm2m.model_path_file:}") + private String modelPathFile; + + @Getter + @Value("${transport.lwm2m.recommended_ciphers:}") + private boolean recommendedCiphers; + + @Getter + @Value("${transport.lwm2m.recommended_supported_groups:}") + private boolean recommendedSupportedGroups; + + @Getter + @Value("${transport.lwm2m.request_pool_size:}") + private int requestPoolSize; + + @Getter + @Value("${transport.lwm2m.request_error_pool_size:}") + private int requestErrorPoolSize; + + @Getter + @Value("${transport.lwm2m.registered_pool_size:}") + private int registeredPoolSize; + + @Getter + @Value("${transport.lwm2m.update_registered_pool_size:}") + private int updateRegisteredPoolSize; + + @Getter + @Value("${transport.lwm2m.un_registered_pool_size:}") + private int unRegisteredPoolSize; + + @Getter + @Value("${transport.lwm2m.secure.key_store_type:}") + private String keyStoreType; + + @Getter + @Value("${transport.lwm2m.secure.key_store_path_file:}") + private String keyStorePathFile; + + @Getter + @Setter + private KeyStore keyStoreValue; + + @Getter + @Value("${transport.lwm2m.secure.key_store_password:}") + private String keyStorePasswordServer; + + @Getter + @Value("${transport.lwm2m.secure.root_alias:}") + private String rootAlias; + + @Getter + @Value("${transport.lwm2m.secure.enable_gen_new_key_psk_rpk:}") + private Boolean enableGenNewKeyPskRpk; + + @Getter + @Value("${transport.lwm2m.server.id:}") + private Integer serverId; + + @Getter + @Value("${transport.lwm2m.server.bind_address:}") + private String serverHost; + + @Getter + @Value("${transport.lwm2m.server.secure.bind_address_security:}") + private String serverHostSecurity; + + @Getter + @Value("${transport.lwm2m.server.bind_port_no_sec:}") + private Integer serverPortNoSec; + + @Getter + @Value("${transport.lwm2m.server.secure.bind_port_security:}") + private Integer serverPortSecurity; + + @Getter + @Value("${transport.lwm2m.server.secure.public_x:}") + private String serverPublicX; + + @Getter + @Value("${transport.lwm2m.server.secure.public_y:}") + private String serverPublicY; + + @Getter + @Value("${transport.lwm2m.server.secure.private_encoded:}") + private String serverPrivateEncoded; + + @Getter + @Value("${transport.lwm2m.server.secure.alias:}") + private String serverAlias; + + @PostConstruct + public void init() { + modelsValue = ObjectLoader.loadDefault(); + File path = getPathModels(); + if (path.isDirectory()) { + try { + modelsValue.addAll(ObjectLoader.loadObjectsFromDir(path)); + log.info(" [{}] Models directory is a directory", path.getAbsoluteFile()); + } catch (Exception e) { + log.error(" [{}] Could not parse the resource definition file", e.toString()); + } + } else { + log.error(" [{}] Read Models", path.getAbsoluteFile()); + } + this.getInKeyStore(); + } + + private File getPathModels() { + Path pathModels = (modelPathFile != null && !modelPathFile.isEmpty()) ? Paths.get(modelPathFile) : + (new File(Paths.get(getBaseDirPath(), PATH_DATA, MODEL_PATH_DEFAULT).toUri()).isDirectory()) ? + Paths.get(getBaseDirPath(), PATH_DATA, MODEL_PATH_DEFAULT) : + Paths.get(getBaseDirPath(), APP_DIR, TRANSPORT_DIR, LWM2M_DIR, SRC_DIR, MAIN_DIR, RESOURCES_DIR, MODEL_PATH_DEFAULT); + return (pathModels != null) ? new File(pathModels.toUri()) : null; + } + + private KeyStore getInKeyStore() { + try { + if (keyStoreValue != null && keyStoreValue.size() > 0) + return keyStoreValue; + } catch (KeyStoreException e) { + log.error("Uninitialized keystore [{}]", keyStoreValue.toString()); + } + Path keyStorePath = (keyStorePathFile != null && !keyStorePathFile.isEmpty()) ? Paths.get(keyStorePathFile) : + (new File(Paths.get(getBaseDirPath(), PATH_DATA, KEY_STORE_DEFAULT_RESOURCE_PATH, KEY_STORE_DEFAULT_FILE).toUri()).isFile()) ? + Paths.get(getBaseDirPath(), PATH_DATA, KEY_STORE_DEFAULT_RESOURCE_PATH, KEY_STORE_DEFAULT_FILE) : + Paths.get(getBaseDirPath(), APP_DIR, TRANSPORT_DIR, LWM2M_DIR, SRC_DIR, MAIN_DIR, RESOURCES_DIR, KEY_STORE_DEFAULT_RESOURCE_PATH, KEY_STORE_DEFAULT_FILE); + File keyStoreFile = new File(keyStorePath.toUri()); + if (keyStoreFile.isFile()) { + try { + InputStream inKeyStore = new FileInputStream(keyStoreFile); + keyStoreValue = KeyStore.getInstance(keyStoreType); + keyStoreValue.load(inKeyStore, keyStorePasswordServer == null ? null : keyStorePasswordServer.toCharArray()); + } catch (CertificateException | NoSuchAlgorithmException | IOException | KeyStoreException e) { + log.error("[{}] Unable to load KeyStore files server, folder is not a directory", e.getMessage()); + keyStoreValue = null; + } + log.info("[{}] Load KeyStore files server, folder is a directory", keyStoreFile.getAbsoluteFile()); + } else { + log.error("[{}] Unable to load KeyStore files server, is not a file", keyStoreFile.getAbsoluteFile()); + keyStoreValue = null; + } + return keyStoreValue; + } + + private String getBaseDirPath() { + Path FULL_FILE_PATH; + if (BASE_DIR_PATH.endsWith("bin")) { + FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("bin$", "")); + } else if (BASE_DIR_PATH.endsWith("conf")) { + FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("conf$", "")); + } else { + FULL_FILE_PATH = Paths.get(BASE_DIR_PATH); + } + return FULL_FILE_PATH.toUri().getPath(); + } + + public ResourceModel getResourceModel(Registration registration, LwM2mPath pathIds) { + String pathLink = "/" + pathIds.getObjectId() + "/" + pathIds.getObjectInstanceId(); + return (Arrays.stream(registration.getObjectLinks()).filter(p-> p.getUrl().equals(pathLink)).findFirst().isPresent() && + this.modelsValue.stream().filter(v -> v.id == pathIds.getObjectId()).collect(Collectors.toList()).size() > 0) && + this.modelsValue.stream().filter(v -> v.id == pathIds.getObjectId()).collect(Collectors.toList()).get(0).resources.containsKey(pathIds.getResourceId()) ? + this.modelsValue.stream().filter(v -> v.id == pathIds.getObjectId()).collect(Collectors.toList()).get(0).resources.get(pathIds.getResourceId()) : + null; + } + + public ResourceModel.Type getResourceModelType(Registration registration, LwM2mPath pathIds) { + ResourceModel resource = this.getResourceModel(registration, pathIds); + return (resource == null) ? null : resource.type; + } + + public ResourceModel.Operations getOperation(Registration registration, LwM2mPath pathIds) { + ResourceModel resource = this.getResourceModel(registration, pathIds); + return (resource == null) ? ResourceModel.Operations.NONE : resource.operations; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java index af41460415..5c36e9b3e0 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportDeviceProfileCache.java @@ -17,27 +17,41 @@ package org.thingsboard.server.common.transport.service; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.transport.TransportDeviceProfileCache; +import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.util.DataDecodingEncodingService; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbTransportComponent; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; @Slf4j @Component @TbTransportComponent public class DefaultTransportDeviceProfileCache implements TransportDeviceProfileCache { + private final Lock deviceProfileFetchLock = new ReentrantLock(); private final ConcurrentMap deviceProfiles = new ConcurrentHashMap<>(); - private final DataDecodingEncodingService dataDecodingEncodingService; + private TransportService transportService; + + @Lazy + @Autowired + public void setTransportService(TransportService transportService) { + this.transportService = transportService; + } + public DefaultTransportDeviceProfileCache(DataDecodingEncodingService dataDecodingEncodingService) { this.dataDecodingEncodingService = dataDecodingEncodingService; } @@ -57,7 +71,7 @@ public class DefaultTransportDeviceProfileCache implements TransportDeviceProfil @Override public DeviceProfile get(DeviceProfileId id) { - return deviceProfiles.get(id); + return this.getDeviceProfile(id); } @Override @@ -80,4 +94,31 @@ public class DefaultTransportDeviceProfileCache implements TransportDeviceProfil public void evict(DeviceProfileId id) { deviceProfiles.remove(id); } + + + private DeviceProfile getDeviceProfile(DeviceProfileId id) { + DeviceProfile profile = deviceProfiles.get(id); + if (profile == null) { + deviceProfileFetchLock.lock(); + try { + TransportProtos.GetEntityProfileRequestMsg msg = TransportProtos.GetEntityProfileRequestMsg.newBuilder() + .setEntityType(EntityType.DEVICE_PROFILE.name()) + .setEntityIdMSB(id.getId().getMostSignificantBits()) + .setEntityIdLSB(id.getId().getLeastSignificantBits()) + .build(); + TransportProtos.GetEntityProfileResponseMsg entityProfileMsg = transportService.getEntityProfile(msg); + Optional profileOpt = dataDecodingEncodingService.decode(entityProfileMsg.getData().toByteArray()); + if (profileOpt.isPresent()) { + profile = profileOpt.get(); + this.put(profile); + } else { + log.warn("[{}] Can't device profile: {}", id, entityProfileMsg.getData()); + throw new RuntimeException("Can't device profile!"); + } + } finally { + deviceProfileFetchLock.unlock(); + } + } + return profile; + } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 5e46a77fb1..b273ee48cf 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -61,6 +61,7 @@ import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; @@ -107,6 +108,8 @@ import java.util.concurrent.atomic.AtomicInteger; @TbTransportComponent public class DefaultTransportService implements TransportService { + public static final String OVERWRITE_ACTIVITY_TIME = "overwriteActivityTime"; + @Value("${transport.sessions.inactivity_timeout}") private long sessionInactivityTimeout; @Value("${transport.sessions.report_timeout}") @@ -193,6 +196,7 @@ public class DefaultTransportService implements TransportService { } records.forEach(record -> { try { + log.info("[{}] SessionIdMSB, [{}] SessionIdLSB, records", record.getValue().getSessionIdMSB(), record.getValue().getSessionIdLSB()); processToTransportMsg(record.getValue()); } catch (Throwable e) { log.warn("Failed to process the notification.", e); @@ -232,8 +236,10 @@ public class DefaultTransportService implements TransportService { } @Override - public void registerAsyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener) { - sessions.putIfAbsent(toSessionId(sessionInfo), new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listener)); + public SessionMetaData registerAsyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener) { + SessionMetaData newValue = new SessionMetaData(sessionInfo, TransportProtos.SessionType.ASYNC, listener); + SessionMetaData oldValue = sessions.putIfAbsent(toSessionId(sessionInfo), newValue); + return oldValue != null ? oldValue : newValue; } @Override @@ -248,6 +254,18 @@ public class DefaultTransportService implements TransportService { } } + @Override + public TransportProtos.GetResourcesResponseMsg getResources(TransportProtos.GetResourcesRequestMsg msg) { + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setResourcesRequestMsg(msg).build()); + try { + TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); + return response.getValue().getResourcesResponseMsg(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + @Override public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceTokenRequestMsg msg, TransportServiceCallback callback) { @@ -267,6 +285,13 @@ public class DefaultTransportService implements TransportService { } @Override + public void process(TransportProtos.ValidateDeviceLwM2MCredentialsRequestMsg msg, TransportServiceCallback callback) { + log.trace("Processing msg: {}", msg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateDeviceLwM2MCredentialsRequestMsg(msg).build()); + AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), + response -> callback.onSuccess(response.getValue().getValidateCredResponseMsg()), callback::onError, transportCallbackExecutor); + } + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateX509CertRequestMsg(msg).build()); @@ -318,6 +343,15 @@ public class DefaultTransportService implements TransportService { AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } + @Override + public void process(TransportProtos.LwM2MRequestMsg msg, TransportServiceCallback callback) { + log.trace("Processing msg: {}", msg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setLwM2MRequestMsg(msg).build()); + AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), + response -> callback.onSuccess(response.getValue().getLwM2MResponseMsg()), callback::onError, transportCallbackExecutor); + } + private TransportDeviceInfo getTransportDeviceInfo(TransportProtos.DeviceInfoProto di) { TransportDeviceInfo tdi = new TransportDeviceInfo(); tdi.setTenantId(new TenantId(new UUID(di.getTenantIdMSB(), di.getTenantIdLSB()))); @@ -339,6 +373,7 @@ public class DefaultTransportService implements TransportService { AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } + @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SubscriptionInfoProto msg, TransportServiceCallback callback) { if (log.isTraceEnabled()) { @@ -491,8 +526,8 @@ public class DefaultTransportService implements TransportService { } @Override - public void reportActivity(TransportProtos.SessionInfoProto sessionInfo) { - reportActivityInternal(sessionInfo); + public SessionMetaData reportActivity(TransportProtos.SessionInfoProto sessionInfo) { + return reportActivityInternal(sessionInfo); } private SessionMetaData reportActivityInternal(TransportProtos.SessionInfoProto sessionInfo) { @@ -512,7 +547,7 @@ public class DefaultTransportService implements TransportService { if (sessionInfo.getGwSessionIdMSB() != 0 && sessionInfo.getGwSessionIdLSB() != 0) { SessionMetaData gwMetaData = sessions.get(new UUID(sessionInfo.getGwSessionIdMSB(), sessionInfo.getGwSessionIdLSB())); - if (gwMetaData != null) { + if (gwMetaData != null && gwMetaData.isOverwriteActivityTime()) { lastActivityTime = Math.max(gwMetaData.getLastActivityTime(), lastActivityTime); } } @@ -546,7 +581,7 @@ public class DefaultTransportService implements TransportService { } @Override - public void registerSyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout) { + public SessionMetaData registerSyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout) { SessionMetaData currentSession = new SessionMetaData(sessionInfo, TransportProtos.SessionType.SYNC, listener); sessions.putIfAbsent(toSessionId(sessionInfo), currentSession); @@ -556,6 +591,7 @@ public class DefaultTransportService implements TransportService { }, timeout, TimeUnit.MILLISECONDS); currentSession.setScheduledFuture(executorFuture); + return currentSession; } @Override @@ -605,6 +641,9 @@ public class DefaultTransportService implements TransportService { if (toSessionMsg.hasSessionCloseNotification()) { listener.onRemoteSessionCloseCommand(toSessionMsg.getSessionCloseNotification()); } + if (toSessionMsg.hasToTransportUpdateCredentialsNotification()) { + listener.onToTransportUpdateCredentials(toSessionMsg.getToTransportUpdateCredentialsNotification()); + } if (toSessionMsg.hasToDeviceRequest()) { listener.onToDeviceRpcRequest(toSessionMsg.getToDeviceRequest()); } @@ -661,6 +700,10 @@ public class DefaultTransportService implements TransportService { } else if (EntityType.DEVICE.equals(entityType)) { rateLimitService.remove(new DeviceId(entityUuid)); } + } else if (toSessionMsg.hasResourceUpdateMsg()) { + //TODO: update resource cache + } else if (toSessionMsg.hasResourceDeleteMsg()) { + //TODO: remove resource from cache } else { //TODO: should we notify the device actor about missed session? log.debug("[{}] Missing session.", sessionId); @@ -668,7 +711,8 @@ public class DefaultTransportService implements TransportService { } } - private void onProfileUpdate(DeviceProfile deviceProfile) { + + public void onProfileUpdate(DeviceProfile deviceProfile) { long deviceProfileIdMSB = deviceProfile.getId().getId().getMostSignificantBits(); long deviceProfileIdLSB = deviceProfile.getId().getId().getLeastSignificantBits(); sessions.forEach((id, md) -> { @@ -707,8 +751,13 @@ public class DefaultTransportService implements TransportService { .setDeviceProfileIdMSB(deviceProfileIdMSB) .setDeviceProfileIdLSB(deviceProfileIdLSB) .setDeviceName(device.getName()) - .setDeviceType(device.getType()) - .build(); + .setDeviceType(device.getType()).build(); + if (device.getAdditionalInfo().has("gateway") + && device.getAdditionalInfo().get("gateway").asBoolean() + && device.getAdditionalInfo().has(OVERWRITE_ACTIVITY_TIME) + && device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).isBoolean()) { + md.setOverwriteActivityTime(device.getAdditionalInfo().get(OVERWRITE_ACTIVITY_TIME).asBoolean()); + } md.setSessionInfo(newSessionInfo); transportCallbackExecutor.submit(() -> md.getListener().onDeviceUpdate(newSessionInfo, device, Optional.ofNullable(newDeviceProfile))); } @@ -752,8 +801,8 @@ public class DefaultTransportService implements TransportService { wrappedCallback); } - protected void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tbMsg.getOriginator()); + private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, tbMsg.getOriginator()); if (log.isTraceEnabled()) { log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, tbMsg.getOriginator(), tpi.getFullTopicName(), tbMsg); } @@ -765,7 +814,7 @@ public class DefaultTransportService implements TransportService { ruleEngineMsgProducer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), wrappedCallback); } - protected void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, + private void sendToRuleEngine(TenantId tenantId, DeviceId deviceId, TransportProtos.SessionInfoProto sessionInfo, JsonObject json, TbMsgMetaData metaData, SessionMsgType sessionMsgType, TbQueueCallback callback) { DeviceProfileId deviceProfileId = new DeviceProfileId(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB())); DeviceProfile deviceProfile = deviceProfileCache.get(deviceProfileId); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java index c81ca05bc1..bb0ed7aa58 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/SessionMetaData.java @@ -25,7 +25,7 @@ import java.util.concurrent.ScheduledFuture; * Created by ashvayka on 15.10.18. */ @Data -class SessionMetaData { +public class SessionMetaData { private volatile TransportProtos.SessionInfoProto sessionInfo; private final TransportProtos.SessionType sessionType; @@ -36,6 +36,7 @@ class SessionMetaData { private volatile long lastReportedActivityTime; private volatile boolean subscribedToAttributes; private volatile boolean subscribedToRPC; + private volatile boolean overwriteActivityTime; SessionMetaData(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.SessionType sessionType, SessionMsgListener listener) { this.sessionInfo = sessionInfo; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java index 73ff79634d..a3b65c2248 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/util/ProtoWithFSTService.java @@ -32,6 +32,7 @@ public class ProtoWithFSTService implements DataDecodingEncodingService { @Override public Optional decode(byte[] byteArray) { try { + @SuppressWarnings("unchecked") T msg = (T) config.asObject(byteArray); return Optional.of(msg); } catch (IllegalArgumentException e) { diff --git a/common/transport/transport-api/src/test/java/JsonConverterTest.java b/common/transport/transport-api/src/test/java/JsonConverterTest.java new file mode 100644 index 0000000000..cedbef50c9 --- /dev/null +++ b/common/transport/transport-api/src/test/java/JsonConverterTest.java @@ -0,0 +1,53 @@ +/** + * Copyright © 2016-2021 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 com.google.gson.JsonParser; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; + +@RunWith(MockitoJUnitRunner.class) +public class JsonConverterTest { + + private static final JsonParser JSON_PARSER = new JsonParser(); + + @Test + public void testParseBigDecimalAsLong() { + var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1E+1}"), 0L); + Assert.assertEquals(10L, result.get(0L).get(0).getLongValue().get().longValue()); + } + + @Test + public void testParseBigDecimalAsDouble() { + var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 101E-1}"), 0L); + Assert.assertEquals(10.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); + } + + @Test + public void testParseAsDouble() { + var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 1.1}"), 0L); + Assert.assertEquals(1.1, result.get(0L).get(0).getDoubleValue().get(), 0.0); + } + + @Test + public void testParseAsLong() { + var result = JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 11}"), 0L); + Assert.assertEquals(11L, result.get(0L).get(0).getLongValue().get().longValue()); + } + +} diff --git a/common/util/pom.xml b/common/util/pom.xml index 1e0ceb049d..6f66da5790 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT common org.thingsboard.common @@ -41,6 +41,14 @@ guava provided + + javax.annotation + javax.annotation-api + + + com.fasterxml.jackson.core + jackson-databind + org.slf4j slf4j-api @@ -64,7 +72,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java similarity index 71% rename from dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java rename to common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 1a56ff54e1..8d498fa853 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.util.mapping; +package org.thingsboard.common.util; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -38,6 +39,15 @@ public class JacksonUtil { } } + public static T convertValue(Object fromValue, TypeReference toValueTypeRef) { + try { + return fromValue != null ? OBJECT_MAPPER.convertValue(fromValue, toValueTypeRef) : null; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("The given object value: " + + fromValue + " cannot be converted to " + toValueTypeRef, e); + } + } + public static T fromString(String string, Class clazz) { try { return string != null ? OBJECT_MAPPER.readValue(string, clazz) : null; @@ -47,6 +57,15 @@ public class JacksonUtil { } } + public static T fromString(String string, TypeReference valueTypeRef) { + try { + return string != null ? OBJECT_MAPPER.readValue(string, valueTypeRef) : null; + } catch (IOException e) { + throw new IllegalArgumentException("The given string value: " + + string + " cannot be transformed to Json object", e); + } + } + public static String toString(Object value) { try { return value != null ? OBJECT_MAPPER.writeValueAsString(value) : null; @@ -72,7 +91,9 @@ public class JacksonUtil { } public static T clone(T value) { - return fromString(toString(value), (Class) value.getClass()); + @SuppressWarnings("unchecked") + Class valueClass = (Class) value.getClass(); + return fromString(toString(value), valueClass); } public static JsonNode valueToTree(T value) { diff --git a/dao/pom.xml b/dao/pom.xml index 1acbef4c42..e1a08cb74d 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard dao @@ -39,6 +39,10 @@ org.thingsboard.common data + + org.thingsboard.common + cache + org.thingsboard.common message @@ -92,7 +96,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java index 445cf1c897..8f7071eadc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java +++ b/dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java @@ -40,11 +40,11 @@ public abstract class DaoUtil { public static PageData toPageData(Page> page) { List data = convertDataList(page.getContent()); - return new PageData(data, page.getTotalPages(), page.getTotalElements(), page.hasNext()); + return new PageData<>(data, page.getTotalPages(), page.getTotalElements(), page.hasNext()); } public static PageData pageToPageData(Page page) { - return new PageData(page.getContent(), page.getTotalPages(), page.getTotalElements(), page.hasNext()); + return new PageData<>(page.getContent(), page.getTotalPages(), page.getTotalElements(), page.hasNext()); } public static Pageable toPageable(PageLink pageLink) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 6c12b5f753..ebbfae0f05 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -19,16 +19,18 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmStatus; 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.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; -import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.Dao; import java.util.Collection; +import java.util.Set; import java.util.UUID; /** @@ -48,4 +50,6 @@ public interface AlarmDao extends Dao { PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds); + + Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set status); } 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 808b6b5325..9af4dc4749 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 @@ -39,10 +39,10 @@ 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.page.PageData; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -60,7 +60,6 @@ import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -307,7 +306,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ )); } return Futures.transform(Futures.successfulAsList(alarmFutures), - alarmInfos -> new PageData(alarmInfos, alarms.getTotalPages(), alarms.getTotalElements(), + alarmInfos -> new PageData<>(alarmInfos, alarms.getTotalPages(), alarms.getTotalElements(), alarms.hasNext()), MoreExecutors.directExecutor()); } return Futures.immediateFuture(alarms); @@ -316,37 +315,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { - TimePageLink nextPageLink = new TimePageLink(100); - boolean hasNext = true; - AlarmSeverity highestSeverity = null; - AlarmQuery query; - while (hasNext && AlarmSeverity.CRITICAL != highestSeverity) { - query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false, null); - PageData alarms = alarmDao.findAlarms(tenantId, query); - if (alarms.hasNext()) { - nextPageLink = nextPageLink.nextPageLink(); - } - AlarmSeverity severity = detectHighestSeverity(alarms.getData()); - if (severity == null) { - continue; - } - if (severity == AlarmSeverity.CRITICAL || highestSeverity == null) { - highestSeverity = severity; - } else { - highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity; - } + Set statusList = null; + if (alarmSearchStatus != null) { + statusList = alarmSearchStatus.getStatuses(); + } else if (alarmStatus != null) { + statusList = Collections.singleton(alarmStatus); } - return highestSeverity; - } - private AlarmSeverity detectHighestSeverity(List alarms) { - if (!alarms.isEmpty()) { - List sorted = new ArrayList(alarms); - sorted.sort(Comparator.comparing(Alarm::getSeverity)); - return sorted.get(0).getSeverity(); - } else { - return null; - } + Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList); + + return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null); } private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) { @@ -393,7 +371,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ private Set getPropagationEntityIds(Alarm alarm) { if (alarm.isPropagate()) { List relations = relationService.findByTo(alarm.getTenantId(), alarm.getId(), RelationTypeGroup.ALARM); - return relations.stream().map(EntityRelation::getFrom).collect(Collectors.toSet()); + Set propagationEntityIds = relations.stream().map(EntityRelation::getFrom).collect(Collectors.toSet()); + propagationEntityIds.add(alarm.getOriginator()); + return propagationEntityIds; } else { return Collections.singleton(alarm.getOriginator()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java new file mode 100644 index 0000000000..10771656e9 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeCacheKey.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2021 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.dao.attributes; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.thingsboard.server.common.data.id.EntityId; + +import java.io.Serializable; + +@EqualsAndHashCode +@Getter +@AllArgsConstructor +public class AttributeCacheKey implements Serializable { + private static final long serialVersionUID = 2013369077925351881L; + + private final String scope; + private final EntityId entityId; + private final String key; + + @Override + public String toString() { + return entityId + "_" + scope + "_" + key; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java new file mode 100644 index 0000000000..b1fc82fe88 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2021 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.dao.attributes; + +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.dao.exception.IncorrectParameterException; +import org.thingsboard.server.dao.service.Validator; + +public class AttributeUtils { + public static void validate(EntityId id, String scope) { + Validator.validateId(id.getId(), "Incorrect id " + id); + Validator.validateString(scope, "Incorrect scope " + scope); + } + + public static void validate(AttributeKvEntry kvEntry) { + if (kvEntry == null) { + throw new IncorrectParameterException("Key value entry can't be null"); + } else if (kvEntry.getDataType() == null) { + throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null"); + } else { + Validator.validateString(kvEntry.getKey(), "Incorrect kvEntry. Key can't be empty"); + Validator.validatePositiveNumber(kvEntry.getLastUpdateTs(), "Incorrect last update ts. Ts should be positive"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java new file mode 100644 index 0000000000..196204cda1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesCacheWrapper.java @@ -0,0 +1,63 @@ +/** + * Copyright © 2016-2021 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.dao.attributes; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; + +import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE; + +@Service +@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "true") +@Primary +@Slf4j +public class AttributesCacheWrapper { + private final Cache attributesCache; + + public AttributesCacheWrapper(CacheManager cacheManager) { + this.attributesCache = cacheManager.getCache(ATTRIBUTES_CACHE); + } + + public Cache.ValueWrapper get(AttributeCacheKey attributeCacheKey) { + try { + return attributesCache.get(attributeCacheKey); + } catch (Exception e) { + log.debug("Failed to retrieve element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage()); + return null; + } + } + + public void put(AttributeCacheKey attributeCacheKey, AttributeKvEntry attributeKvEntry) { + try { + attributesCache.put(attributeCacheKey, attributeKvEntry); + } catch (Exception e) { + log.debug("Failed to put element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage()); + } + } + + public void evict(AttributeCacheKey attributeCacheKey) { + try { + attributesCache.evict(attributeCacheKey); + } catch (Exception e) { + log.debug("Failed to evict element from cache for key {}. Reason - {}.", attributeCacheKey, e.getMessage()); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index e70604353a..3eee300627 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -15,31 +15,39 @@ */ package org.thingsboard.server.dao.attributes; -import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.Validator; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; + +import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; /** * @author Andrew Shvayka */ @Service +@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "false", matchIfMissing = true) +@Primary +@Slf4j public class BaseAttributesService implements AttributesService { + private final AttributesDao attributesDao; - @Autowired - private AttributesDao attributesDao; + public BaseAttributesService(AttributesDao attributesDao) { + this.attributesDao = attributesDao; + } @Override public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { @@ -75,33 +83,14 @@ public class BaseAttributesService implements AttributesService { public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); attributes.forEach(attribute -> validate(attribute)); - List> futures = Lists.newArrayListWithExpectedSize(attributes.size()); - for (AttributeKvEntry attribute : attributes) { - futures.add(attributesDao.save(tenantId, entityId, scope, attribute)); - } - return Futures.allAsList(futures); + + List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); + return Futures.allAsList(saveFutures); } @Override - public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List keys) { + public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { validate(entityId, scope); - return attributesDao.removeAll(tenantId, entityId, scope, keys); - } - - private static void validate(EntityId id, String scope) { - Validator.validateId(id.getId(), "Incorrect id " + id); - Validator.validateString(scope, "Incorrect scope " + scope); - } - - private static void validate(AttributeKvEntry kvEntry) { - if (kvEntry == null) { - throw new IncorrectParameterException("Key value entry can't be null"); - } else if (kvEntry.getDataType() == null) { - throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null"); - } else { - Validator.validateString(kvEntry.getKey(), "Incorrect kvEntry. Key can't be empty"); - Validator.validatePositiveNumber(kvEntry.getLastUpdateTs(), "Incorrect last update ts. Ts should be positive"); - } + return attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java new file mode 100644 index 0000000000..87ddfeee9a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -0,0 +1,193 @@ +/** + * Copyright © 2016-2021 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.dao.attributes; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.stats.DefaultCounter; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.dao.service.Validator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE; +import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; + +@Service +@ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "true") +@Primary +@Slf4j +public class CachedAttributesService implements AttributesService { + private static final String STATS_NAME = "attributes.cache"; + + private final AttributesDao attributesDao; + private final AttributesCacheWrapper cacheWrapper; + private final DefaultCounter hitCounter; + private final DefaultCounter missCounter; + + public CachedAttributesService(AttributesDao attributesDao, + AttributesCacheWrapper cacheWrapper, + StatsFactory statsFactory) { + this.attributesDao = attributesDao; + this.cacheWrapper = cacheWrapper; + + this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit"); + this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss"); + } + + @Override + public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, String attributeKey) { + validate(entityId, scope); + Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); + + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, attributeKey); + Cache.ValueWrapper cachedAttributeValue = cacheWrapper.get(attributeCacheKey); + if (cachedAttributeValue != null) { + hitCounter.increment(); + AttributeKvEntry cachedAttributeKvEntry = (AttributeKvEntry) cachedAttributeValue.get(); + return Futures.immediateFuture(Optional.ofNullable(cachedAttributeKvEntry)); + } else { + missCounter.increment(); + ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, attributeKey); + return Futures.transform(result, foundAttrKvEntry -> { + // TODO: think if it's a good idea to store 'empty' attributes + cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null)); + return foundAttrKvEntry; + }, MoreExecutors.directExecutor()); + } + } + + @Override + public ListenableFuture> find(TenantId tenantId, EntityId entityId, String scope, Collection attributeKeys) { + validate(entityId, scope); + attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey)); + + Map wrappedCachedAttributes = findCachedAttributes(entityId, scope, attributeKeys); + + List cachedAttributes = wrappedCachedAttributes.values().stream() + .map(wrappedCachedAttribute -> (AttributeKvEntry) wrappedCachedAttribute.get()) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (wrappedCachedAttributes.size() == attributeKeys.size()) { + return Futures.immediateFuture(cachedAttributes); + } + + Set notFoundAttributeKeys = new HashSet<>(attributeKeys); + notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); + + ListenableFuture> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); + return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), MoreExecutors.directExecutor()); + + } + + private Map findCachedAttributes(EntityId entityId, String scope, Collection attributeKeys) { + Map cachedAttributes = new HashMap<>(); + for (String attributeKey : attributeKeys) { + Cache.ValueWrapper cachedAttributeValue = cacheWrapper.get(new AttributeCacheKey(scope, entityId, attributeKey)); + if (cachedAttributeValue != null) { + hitCounter.increment(); + cachedAttributes.put(attributeKey, cachedAttributeValue); + } else { + missCounter.increment(); + } + } + return cachedAttributes; + } + + private List mergeDbAndCacheAttributes(EntityId entityId, String scope, List cachedAttributes, Set notFoundAttributeKeys, List foundInDbAttributes) { + for (AttributeKvEntry foundInDbAttribute : foundInDbAttributes) { + AttributeCacheKey attributeCacheKey = new AttributeCacheKey(scope, entityId, foundInDbAttribute.getKey()); + cacheWrapper.put(attributeCacheKey, foundInDbAttribute); + notFoundAttributeKeys.remove(foundInDbAttribute.getKey()); + } + for (String key : notFoundAttributeKeys){ + cacheWrapper.put(new AttributeCacheKey(scope, entityId, key), null); + } + List mergedAttributes = new ArrayList<>(cachedAttributes); + mergedAttributes.addAll(foundInDbAttributes); + return mergedAttributes; + } + + @Override + public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, String scope) { + validate(entityId, scope); + return attributesDao.findAll(tenantId, entityId, scope); + } + + @Override + public List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId) { + return attributesDao.findAllKeysByDeviceProfileId(tenantId, deviceProfileId); + } + + @Override + public List findAllKeysByEntityIds(TenantId tenantId, EntityType entityType, List entityIds) { + return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds); + } + + @Override + public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { + validate(entityId, scope); + attributes.forEach(AttributeUtils::validate); + + List> saveFutures = attributes.stream().map(attribute -> attributesDao.save(tenantId, entityId, scope, attribute)).collect(Collectors.toList()); + ListenableFuture> future = Futures.allAsList(saveFutures); + + // TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache + List attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor()); + return future; + } + + @Override + public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { + validate(entityId, scope); + ListenableFuture> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor()); + return future; + } + + private void evictAttributesFromCache(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { + try { + for (String attributeKey : attributeKeys) { + cacheWrapper.evict(new AttributeCacheKey(scope, entityId, attributeKey)); + } + } catch (Exception e) { + log.error("[{}][{}] Failed to remove values from cache.", tenantId, entityId, e); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 8e0490b2c7..7bf13871c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -49,7 +49,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import java.io.PrintWriter; import java.io.StringWriter; @@ -190,6 +190,7 @@ public class AuditLogServiceImpl implements AuditLogService { case ATTRIBUTES_UPDATED: actionData.put("entityId", entityId.toString()); String scope = extractParameter(String.class, 0, additionalInfo); + @SuppressWarnings("unchecked") List attributes = extractParameter(List.class, 1, additionalInfo); actionData.put("scope", scope); ObjectNode attrsNode = JacksonUtil.newObjectNode(); @@ -205,6 +206,7 @@ public class AuditLogServiceImpl implements AuditLogService { actionData.put("entityId", entityId.toString()); scope = extractParameter(String.class, 0, additionalInfo); actionData.put("scope", scope); + @SuppressWarnings("unchecked") List keys = extractParameter(List.class, 1, additionalInfo); ArrayNode attrsArrayNode = actionData.putArray("attributes"); if (keys != null) { @@ -267,6 +269,7 @@ public class AuditLogServiceImpl implements AuditLogService { break; case TIMESERIES_UPDATED: actionData.put("entityId", entityId.toString()); + @SuppressWarnings("unchecked") List updatedTimeseries = extractParameter(List.class, 0, additionalInfo); if (updatedTimeseries != null) { ArrayNode result = actionData.putArray("timeseries"); @@ -283,6 +286,7 @@ public class AuditLogServiceImpl implements AuditLogService { break; case TIMESERIES_DELETED: actionData.put("entityId", entityId.toString()); + @SuppressWarnings("unchecked") List timeseriesKeys = extractParameter(List.class, 0, additionalInfo); if (timeseriesKeys != null) { ArrayNode timeseriesArrayNode = actionData.putArray("timeseries"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java index e19e580b9e..46190464db 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java @@ -36,22 +36,22 @@ public class DummyAuditLogServiceImpl implements AuditLogService { @Override public PageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, List actionTypes, TimePageLink pageLink) { - return new PageData(); + return new PageData<>(); } @Override public PageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, List actionTypes, TimePageLink pageLink) { - return new PageData(); + return new PageData<>(); } @Override public PageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, List actionTypes, TimePageLink pageLink) { - return new PageData(); + return new PageData<>(); } @Override public PageData findAuditLogsByTenantId(TenantId tenantId, List actionTypes, TimePageLink pageLink) { - return new PageData(); + return new PageData<>(); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java b/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java index b0c5a5b74f..038ff2bd23 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.cache; import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -24,6 +25,7 @@ import java.lang.reflect.Method; import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; +@Component("previousDeviceCredentialsId") public class PreviousDeviceCredentialsIdKeyGenerator implements KeyGenerator { private static final String NOT_VALID_DEVICE = DEVICE_CREDENTIALS_CACHE + "_notValidDeviceCredentialsId"; 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 9e0af81823..f9b3b23ff0 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 @@ -21,7 +21,6 @@ import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; @@ -34,7 +33,7 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -87,6 +86,9 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen case MQTT_BASIC: formatSimpleMqttCredentials(deviceCredentials); break; + case LWM2M_CREDENTIALS: + formatSimpleLwm2mCredentials(deviceCredentials); + break; } log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials, id -> tenantId); @@ -129,6 +131,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); } + private void formatCertData(DeviceCredentials deviceCredentials) { String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue()); String sha3Hash = EncryptionUtil.getSha3Hash(cert); @@ -136,6 +139,10 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen deviceCredentials.setCredentialsValue(cert); } + private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) { + + } + @Override @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId") public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) { 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 c088d18d3c..f933cbd543 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 @@ -72,7 +72,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import javax.annotation.Nullable; import java.util.ArrayList; @@ -178,10 +178,40 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return doSaveDevice(device, null); } + @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}") + @Override + public Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials) { + if (device.getId() == null) { + Device deviceWithName = this.findDeviceByTenantIdAndName(device.getTenantId(), device.getName()); + device = deviceWithName == null ? device : deviceWithName.updateDevice(device); + } + Device savedDevice = this.saveDeviceWithoutCredentials(device); + deviceCredentials.setDeviceId(savedDevice.getId()); + if (device.getId() == null) { + deviceCredentials = deviceCredentialsService.createDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); + } + else { + deviceCredentials.setId(deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), savedDevice.getId()).getId()); + deviceCredentials = deviceCredentialsService.updateDeviceCredentials(device.getTenantId(), deviceCredentials); + } + return savedDevice; + } + private Device doSaveDevice(Device device, String accessToken) { + Device savedDevice = this.saveDeviceWithoutCredentials(device); + if (device.getId() == null) { + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + deviceCredentials.setCredentialsId(!StringUtils.isEmpty(accessToken) ? accessToken : RandomStringUtils.randomAlphanumeric(20)); + deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); + } + return savedDevice; + } + + private Device saveDeviceWithoutCredentials(Device device) { log.trace("Executing saveDevice [{}]", device); deviceValidator.validate(device, Device::getTenantId); - Device savedDevice; try { DeviceProfile deviceProfile; if (device.getDeviceProfileId() == null) { @@ -199,24 +229,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } device.setType(deviceProfile.getName()); device.setDeviceData(syncDeviceData(deviceProfile, device.getDeviceData())); - - savedDevice = deviceDao.save(device.getTenantId(), device); + return deviceDao.save(device.getTenantId(), device); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_name_unq_key")) { + // remove device from cache in case null value cached in the distributed redis. + removeDeviceFromCache(device.getTenantId(), device.getName()); throw new DataValidationException("Device with such name already exists!"); } else { throw t; } } - if (device.getId() == null) { - DeviceCredentials deviceCredentials = new DeviceCredentials(); - deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); - deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); - deviceCredentials.setCredentialsId(!StringUtils.isEmpty(accessToken) ? accessToken : RandomStringUtils.randomAlphanumeric(20)); - deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); - } - return savedDevice; } private DeviceData syncDeviceData(DeviceProfile deviceProfile, DeviceData deviceData) { @@ -284,13 +307,17 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe } deleteEntityRelations(tenantId, deviceId); + removeDeviceFromCache(tenantId, device.getName()); + + deviceDao.removeById(tenantId, deviceId.getId()); + } + + private void removeDeviceFromCache(TenantId tenantId, String name) { List list = new ArrayList<>(); - list.add(device.getTenantId()); - list.add(device.getName()); + list.add(tenantId); + list.add(name); Cache cache = cacheManager.getCache(DEVICE_CACHE); cache.evict(list); - - deviceDao.removeById(tenantId, deviceId.getId()); } @Override @@ -515,6 +542,8 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe case X509_CERTIFICATE: deviceCredentials.setCredentialsValue(provisionRequest.getCredentialsData().getX509CertHash()); break; + case LWM2M_CREDENTIALS: + break; } try { deviceCredentialsService.updateDeviceCredentials(savedDevice.getTenantId(), deviceCredentials); @@ -531,7 +560,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Override protected void validateCreate(TenantId tenantId, Device device) { DefaultTenantProfileConfiguration profileConfiguration = - (DefaultTenantProfileConfiguration)tenantProfileCache.get(tenantId).getProfileData().getConfiguration(); + (DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration(); long maxDevices = profileConfiguration.getMaxDevices(); validateNumberOfEntitiesPerTenant(tenantId, deviceDao, maxDevices, EntityType.DEVICE); } 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 db7dc6bd2d..5b6ae32437 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 @@ -275,6 +275,7 @@ public class EntityViewServiceImpl extends AbstractEntityService implements Enti tenantIdAndEntityId.add(entityId); Cache cache = cacheManager.getCache(ENTITY_VIEW_CACHE); + @SuppressWarnings("unchecked") List fromCache = cache.get(tenantIdAndEntityId, List.class); if (fromCache != null) { return Futures.immediateFuture(fromCache); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index fc6a2bed25..3f11a4ed00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -453,6 +453,15 @@ public class ModelConstants { public static final String API_USAGE_STATE_EMAIL_EXEC_COLUMN = "email_exec"; public static final String API_USAGE_STATE_SMS_EXEC_COLUMN = "sms_exec"; + /** + * Resource constants. + */ + public static final String RESOURCE_TABLE_NAME = "resource"; + public static final String RESOURCE_TENANT_ID_COLUMN = TENANT_ID_COLUMN; + public static final String RESOURCE_TYPE_COLUMN = "resource_type"; + public static final String RESOURCE_ID_COLUMN = "resource_id"; + public static final String RESOURCE_VALUE_COLUMN = "resource_value"; + /** * Cassandra attributes and timeseries constants. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java index 956a244b7e..5a4381fa24 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractDeviceEntity.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import org.thingsboard.server.dao.util.mapping.JsonStringType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java index b71f608ef1..0ddf78c8ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceProfileEntity.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import javax.persistence.Column; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceCompositeKey.java new file mode 100644 index 0000000000..828f1f4341 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceCompositeKey.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2021 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.dao.model.sql; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.transport.resource.Resource; + +import javax.persistence.Transient; +import java.io.Serializable; +import java.util.UUID; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class ResourceCompositeKey implements Serializable { + + @Transient + private static final long serialVersionUID = -3789469030818742769L; + + private UUID tenantId; + private String resourceType; + private String resourceId; + + public ResourceCompositeKey(Resource resource) { + this.tenantId = resource.getTenantId().getId(); + this.resourceType = resource.getResourceType().name(); + this.resourceId = resource.getResourceId(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java new file mode 100644 index 0000000000..a877afa027 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ResourceEntity.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2021 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.dao.model.sql; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; +import org.thingsboard.server.dao.model.ToData; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TYPE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_VALUE_COLUMN; + +@Data +@Entity +@Table(name = RESOURCE_TABLE_NAME) +@IdClass(ResourceCompositeKey.class) +public class ResourceEntity implements ToData { + + @Id + @Column(name = RESOURCE_TENANT_ID_COLUMN, columnDefinition = "uuid") + private UUID tenantId; + + @Id + @Column(name = RESOURCE_TYPE_COLUMN) + private String resourceType; + + @Id + @Column(name = RESOURCE_ID_COLUMN) + private String resourceId; + + @Column(name = RESOURCE_VALUE_COLUMN) + private String value; + + public ResourceEntity() { + } + + public ResourceEntity(Resource resource) { + this.tenantId = resource.getTenantId().getId(); + this.resourceType = resource.getResourceType().name(); + this.resourceId = resource.getResourceId(); + this.value = resource.getValue(); + } + + @Override + public Resource toData() { + Resource resource = new Resource(); + resource.setTenantId(new TenantId(tenantId)); + resource.setResourceType(ResourceType.valueOf(resourceType)); + resource.setResourceId(resourceId); + resource.setValue(value); + return resource; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java index 0db72f317b..4749af9f6c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import javax.persistence.Column; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index 1639074338..7361dc21d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -53,7 +53,7 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep .userNameAttributeName(localClientRegistration.getUserNameAttributeName()) .jwkSetUri(localClientRegistration.getJwkSetUri()) .clientAuthenticationMethod(new ClientAuthenticationMethod(localClientRegistration.getClientAuthenticationMethod())) - .redirectUriTemplate(defaultRedirectUriTemplate) + .redirectUri(defaultRedirectUriTemplate) .build(); } } 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 9b936b94e8..44b328fd9f 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 @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.entity.EntityService; @@ -301,6 +301,7 @@ public class BaseRelationService implements RelationService { fromAndTypeGroup.add(EntitySearchDirection.FROM.name()); Cache cache = cacheManager.getCache(RELATIONS_CACHE); + @SuppressWarnings("unchecked") List fromCache = cache.get(fromAndTypeGroup, List.class); if (fromCache != null) { return Futures.immediateFuture(fromCache); @@ -382,6 +383,7 @@ public class BaseRelationService implements RelationService { toAndTypeGroup.add(EntitySearchDirection.TO.name()); Cache cache = cacheManager.getCache(RELATIONS_CACHE); + @SuppressWarnings("unchecked") List fromCache = cache.get(toAndTypeGroup, List.class); if (fromCache != null) { return Futures.immediateFuture(fromCache); @@ -455,7 +457,7 @@ public class BaseRelationService implements RelationService { //boolean fetchLastLevelOnly = true; log.trace("Executing findByQuery [{}]", query); RelationsSearchParameters params = query.getParameters(); - final List filters = query.getFilters(); + final List filters = query.getFilters(); if (filters == null || filters.isEmpty()) { log.debug("Filters are not set [{}]", query); } @@ -573,8 +575,8 @@ public class BaseRelationService implements RelationService { }; } - private boolean matchFilters(List filters, EntityRelation relation, EntitySearchDirection direction) { - for (EntityTypeFilter filter : filters) { + private boolean matchFilters(List filters, EntityRelation relation, EntitySearchDirection direction) { + for (RelationEntityTypeFilter filter : filters) { if (match(filter, relation, direction)) { return true; } @@ -582,7 +584,7 @@ public class BaseRelationService implements RelationService { return false; } - private boolean match(EntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) { + private boolean match(RelationEntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) { if (StringUtils.isEmpty(filter.getRelationType()) || filter.getRelationType().equals(relation.getType())) { if (filter.getEntityTypes() == null || filter.getEntityTypes().isEmpty()) { return true; 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 new file mode 100644 index 0000000000..f5f23836b6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2021 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.dao.resource; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +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.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service +@Slf4j +public class BaseResourceService implements ResourceService { + + private final ResourceDao resourceDao; + + public BaseResourceService(ResourceDao resourceDao) { + this.resourceDao = resourceDao; + } + + @Override + public Resource saveResource(Resource resource) { + log.trace("Executing saveResource [{}]", resource); + validate(resource); + return resourceDao.saveResource(resource); + } + + @Override + public Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId) { + log.trace("Executing getResource [{}] [{}] [{}]", tenantId, resourceType, resourceId); + validate(tenantId, resourceType, resourceId); + return resourceDao.getResource(tenantId, resourceType, resourceId); + } + + @Override + public void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId) { + log.trace("Executing deleteResource [{}] [{}] [{}]", tenantId, resourceType, resourceId); + validate(tenantId, resourceType, resourceId); + resourceDao.deleteResource(tenantId, resourceType, resourceId); + } + + @Override + public PageData findResourcesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findByTenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + return resourceDao.findAllByTenantId(tenantId, pageLink); + } + + @Override + public void deleteResourcesByTenantId(TenantId tenantId) { + log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + resourceDao.removeAllByTenantId(tenantId); + } + + protected void validate(Resource resource) { + if (resource == null) { + throw new DataValidationException("Resource should be specified!"); + } + + if (resource.getValue() == null) { + throw new DataValidationException("Resource value should be specified!"); + } + validate(resource.getTenantId(), resource.getResourceType(), resource.getResourceId()); + } + + protected void validate(TenantId tenantId, ResourceType resourceType, String resourceId) { + if (resourceType == null) { + throw new DataValidationException("Resource type should be specified!"); + } + if (resourceId == null) { + throw new DataValidationException("Resource id should be specified!"); + } + validateId(tenantId, "Incorrect tenantId "); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java new file mode 100644 index 0000000000..f8fae8b19d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/ResourceDao.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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.dao.resource; + +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.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; + +public interface ResourceDao { + + Resource saveResource(Resource resource); + + Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); + + void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId); + + PageData findAllByTenantId(TenantId tenantId, PageLink pageLink); + + void removeAllByTenantId(TenantId tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index f3206f30de..f34048c5ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -75,4 +76,12 @@ public interface AlarmRepository extends CrudRepository { @Param("searchText") String searchText, Pageable pageable); + @Query("SELECT alarm.severity FROM AlarmEntity alarm" + + " WHERE alarm.tenantId = :tenantId" + + " AND alarm.originatorId = :entityId" + + " AND ((:status) IS NULL OR alarm.status in (:status))") + Set findAlarmSeverities(@Param("tenantId") UUID tenantId, + @Param("entityId") UUID entityId, + @Param("status") Set status); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index d74092ac95..722928e1b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -24,6 +24,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -120,4 +121,9 @@ public class JpaAlarmDao extends JpaAbstractDao implements A public PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds) { return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds); } + + @Override + public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set status) { + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 3e6c5818ad..6d026327dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -45,12 +45,12 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao getEntityClass() { return DashboardInfoEntity.class; } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return dashboardInfoRepository; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index 70136aa216..92085d44f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql.device; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 8e8bd6638e..4907c03dea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -40,12 +40,13 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityListFilter; import org.thingsboard.server.common.data.query.EntityNameFilter; import org.thingsboard.server.common.data.query.EntitySearchQueryFilter; +import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.query.EntityViewSearchQueryFilter; import org.thingsboard.server.common.data.query.EntityViewTypeFilter; import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.common.data.query.SingleEntityFilter; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.Arrays; import java.util.Collections; @@ -249,18 +250,70 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { EntityType entityType = resolveEntityType(query.getEntityFilter()); QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType)); - ctx.append("select count(e.id) from "); - ctx.append(addEntityTableQuery(ctx, query.getEntityFilter())); - ctx.append(" e where "); - ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList())); - return transactionTemplate.execute(status -> { - long startTs = System.currentTimeMillis(); - try { - return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class); - } finally { - queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); + if (query.getKeyFilters() == null || query.getKeyFilters().isEmpty()) { + ctx.append("select count(e.id) from "); + ctx.append(addEntityTableQuery(ctx, query.getEntityFilter())); + ctx.append(" e where "); + ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList())); + return transactionTemplate.execute(status -> { + long startTs = System.currentTimeMillis(); + try { + return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class); + } finally { + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); + } + }); + } else { + List mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query); + + List selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection) + .collect(Collectors.toList()); + List entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + + List filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter) + .collect(Collectors.toList()); + List entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest()) + .collect(Collectors.toList()); + + List allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest) + .collect(Collectors.toList()); + + + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping); + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true); + String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType); + String entityTypeStr; + if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) { + entityTypeStr = "e.entity_type"; + } else { + entityTypeStr = "'" + entityType.name() + "'"; } - }); + if (!StringUtils.isEmpty(entityFieldsSelection)) { + entityFieldsSelection = String.format("e.id id, %s entity_type, %s", entityTypeStr, entityFieldsSelection); + } else { + entityFieldsSelection = String.format("e.id id, %s entity_type", entityTypeStr); + } + + String fromClauseCount = String.format("from (select %s from (select %s from %s e where %s) entities %s ) result %s", + "entities.*", + entityFieldsSelection, + addEntityTableQuery(ctx, query.getEntityFilter()), + entityWhereClause, + latestJoinsCnt, + ""); + + String countQuery = String.format("select count(id) %s", fromClauseCount); + + return transactionTemplate.execute(status -> { + long startTs = System.currentTimeMillis(); + try { + return jdbcTemplate.queryForObject(countQuery, ctx, Long.class); + } finally { + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); + } + }); + } } @Override @@ -436,6 +489,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { case ASSET_SEARCH_QUERY: case ENTITY_VIEW_SEARCH_QUERY: case API_USAGE_STATE: + case ENTITY_TYPE: return ""; default: throw new RuntimeException("Not implemented!"); @@ -521,7 +575,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { boolean single = entityFilter.getFilters() != null && entityFilter.getFilters().size() == 1; if (entityFilter.getFilters() != null && !entityFilter.getFilters().isEmpty()) { int entityTypeFilterIdx = 0; - for (EntityTypeFilter etf : entityFilter.getFilters()) { + for (RelationEntityTypeFilter etf : entityFilter.getFilters()) { String etfCondition = buildEtfCondition(ctx, etf, entityFilter.getDirection(), entityTypeFilterIdx++); if (!etfCondition.isEmpty()) { if (noConditions) { @@ -570,7 +624,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return "( " + selectFields + from + ")"; } - private String buildEtfCondition(QueryContext ctx, EntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) { + private String buildEtfCondition(QueryContext ctx, RelationEntityTypeFilter etf, EntitySearchDirection direction, int entityTypeFilterIdx) { StringBuilder whereFilter = new StringBuilder(); String relationType = etf.getRelationType(); List entityTypes = etf.getEntityTypes(); @@ -676,6 +730,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return ((EntityListFilter) entityFilter).getEntityType(); case ENTITY_NAME: return ((EntityNameFilter) entityFilter).getEntityType(); + case ENTITY_TYPE: + return ((EntityTypeFilter) entityFilter).getEntityType(); case ASSET_TYPE: case ASSET_SEARCH_QUERY: return EntityType.ASSET; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java index 230c23b604..785ba69c7e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java @@ -83,7 +83,7 @@ public class EntityDataAdapter { if (value != null) { String strVal = value.toString(); // check number - if (strVal.length() > 0 && NumberUtils.isParsable(strVal)) { + if (NumberUtils.isNumber(strVal)) { try { long longVal = Long.parseLong(strVal); return Long.toString(longVal); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 0bacdf6349..313a44f4a3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; +import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityFilter; @@ -380,6 +381,30 @@ public class EntityKeyMapping { return mappings; } + public static List prepareEntityCountKeyMapping(EntityCountQuery query) { + Map> filters = + query.getKeyFilters() != null ? + query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap(); + int index = 2; + List mappings = new ArrayList<>(); + if (!filters.isEmpty()) { + for (EntityKey filterField : filters.keySet()) { + EntityKeyMapping mapping = new EntityKeyMapping(); + mapping.setIndex(index); + mapping.setAlias(String.format("alias%s", index)); + mapping.setKeyFilters(filters.get(filterField)); + mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD)); + mapping.setSelection(false); + mapping.setEntityKey(filterField); + mappings.add(mapping); + index += 1; + } + } + + return mappings; + } + + private String buildAttributeSelection() { return buildTimeSeriesOrAttrSelection(true); } 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 a27b39ced0..c5a1774ae6 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 @@ -49,7 +49,7 @@ public interface RelationRepository String fromType); @Transactional - RelationEntity save(RelationEntity entity); + S save(S entity); @Transactional void deleteById(RelationCompositeKey id); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java new file mode 100644 index 0000000000..0f97d51f8a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceDaoImpl.java @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2021 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.dao.sql.resource; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +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.common.data.transport.resource.Resource; +import org.thingsboard.server.common.data.transport.resource.ResourceType; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.ResourceCompositeKey; +import org.thingsboard.server.dao.model.sql.ResourceEntity; +import org.thingsboard.server.dao.resource.ResourceDao; + +@Slf4j +@Component +public class ResourceDaoImpl implements ResourceDao { + + private final ResourceRepository resourceRepository; + + public ResourceDaoImpl(ResourceRepository resourceRepository) { + this.resourceRepository = resourceRepository; + } + + @Override + @Transactional + public Resource saveResource(Resource resource) { + return DaoUtil.getData(resourceRepository.save(new ResourceEntity(resource))); + } + + @Override + public Resource getResource(TenantId tenantId, ResourceType resourceType, String resourceId) { + ResourceCompositeKey key = new ResourceCompositeKey(); + key.setTenantId(tenantId.getId()); + key.setResourceType(resourceType.name()); + key.setResourceId(resourceId); + + return DaoUtil.getData(resourceRepository.findById(key)); + } + + @Override + @Transactional + public void deleteResource(TenantId tenantId, ResourceType resourceType, String resourceId) { + ResourceCompositeKey key = new ResourceCompositeKey(); + key.setTenantId(tenantId.getId()); + key.setResourceType(resourceType.name()); + key.setResourceId(resourceId); + + resourceRepository.deleteById(key); + } + + @Override + public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(resourceRepository.findAllByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public void removeAllByTenantId(TenantId tenantId) { + resourceRepository.removeAllByTenantId(tenantId.getId()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java new file mode 100644 index 0000000000..ba7472d7ec --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/ResourceRepository.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 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.dao.sql.resource; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.CrudRepository; +import org.thingsboard.server.dao.model.sql.ResourceCompositeKey; +import org.thingsboard.server.dao.model.sql.ResourceEntity; + +import java.util.UUID; + +public interface ResourceRepository extends CrudRepository { + + Page findAllByTenantId(UUID tenantId, Pageable pageable); + + void removeAllByTenantId(UUID tenantId); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 0a750778e3..66907fea5f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -39,12 +39,12 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao getEntityClass() { return RuleChainEntity.class; } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return ruleChainRepository; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index b99d26ebe0..362299f9b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -24,6 +24,8 @@ import org.thingsboard.server.dao.model.sql.RuleNodeEntity; import org.thingsboard.server.dao.rule.RuleNodeDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import java.util.UUID; + @Slf4j @Component public class JpaRuleNodeDao extends JpaAbstractSearchTextDao implements RuleNodeDao { @@ -32,12 +34,12 @@ public class JpaRuleNodeDao extends JpaAbstractSearchTextDao getEntityClass() { return RuleNodeEntity.class; } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return ruleNodeRepository; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java index 3f1647b155..2be895bf86 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeStateDao.java @@ -39,12 +39,12 @@ public class JpaRuleNodeStateDao extends JpaAbstractDao getEntityClass() { return RuleNodeStateEntity.class; } @Override - protected CrudRepository getCrudRepository() { + protected CrudRepository getCrudRepository() { return ruleNodeStateRepository; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java index 4bf2f48a85..09e4bb55ae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java @@ -18,6 +18,8 @@ package org.thingsboard.server.dao.sql.rule; import org.springframework.data.repository.CrudRepository; import org.thingsboard.server.dao.model.sql.RuleNodeEntity; -public interface RuleNodeRepository extends CrudRepository { +import java.util.UUID; + +public interface RuleNodeRepository extends CrudRepository { } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java index 93a7cf5ec7..64c074fd40 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java @@ -19,8 +19,10 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -114,6 +116,14 @@ public class JpaPsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDa partitioningRepository.save(psqlPartition); log.trace("Adding partition to Set: {}", psqlPartition); partitions.put(psqlPartition.getStart(), psqlPartition); + } catch (DataIntegrityViolationException ex) { + log.trace("Error occurred during partition save:", ex); + if (ex.getCause() instanceof ConstraintViolationException) { + log.warn("Saving partition [{}] rejected. Timeseries data will save to the ts_kv_indefinite (DEFAULT) partition.", psqlPartition.getPartitionDate()); + partitions.put(psqlPartition.getStart(), psqlPartition); + } else { + throw new RuntimeException(ex); + } } finally { partitionCreationLock.unlock(); } 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 12fdd3a57a..3016474f27 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 @@ -35,6 +35,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -88,6 +89,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private RuleChainService ruleChainService; + @Autowired + private ResourceService resourceService; + @Override public Tenant findTenantById(TenantId tenantId) { log.trace("Executing findTenantById [{}]", tenantId); @@ -140,6 +144,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe userService.deleteTenantAdmins(tenantId); ruleChainService.deleteRuleChainsByTenantId(tenantId); apiUsageStateService.deleteApiUsageStateByTenantId(tenantId); + resourceService.deleteResourcesByTenantId(tenantId); tenantDao.removeById(tenantId, tenantId.getId()); deleteEntityRelations(tenantId, tenantId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index e7b951f16d..2f817634c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -108,7 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private PreparedStatement deletePartitionStmt; private boolean isInstall() { - return environment.acceptsProfiles("install"); + return environment.acceptsProfiles(Profiles.of("install")); } @PostConstruct @@ -116,16 +117,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD super.startExecutor(); if (!isInstall()) { getFetchStmt(Aggregation.NONE, DESC_ORDER); - Optional partition = NoSqlTsPartitionDate.parse(partitioning); - if (partition.isPresent()) { - tsFormat = partition.get(); - if (!isFixedPartitioning() && partitionsCacheSize > 0) { - cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize); - } - } else { - log.warn("Incorrect configuration of partitioning {}", partitioning); - throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); + } + Optional partition = NoSqlTsPartitionDate.parse(partitioning); + if (partition.isPresent()) { + tsFormat = partition.get(); + if (!isFixedPartitioning() && partitionsCacheSize > 0) { + cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize); } + } else { + log.warn("Incorrect configuration of partitioning {}", partitioning); + throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); } } 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 1f25a0f290..5cb4c693fc 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 @@ -15,8 +15,8 @@ */ package org.thingsboard.server.dao.user; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; @@ -48,6 +48,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.common.util.JacksonUtil; import java.util.HashMap; import java.util.Map; @@ -71,8 +72,6 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private static final String USER_CREDENTIALS_ENABLED = "userCredentialsEnabled"; - private static final ObjectMapper objectMapper = new ObjectMapper(); - @Value("${security.user_login_case_sensitive:true}") private boolean userLoginCaseSensitive; @@ -186,6 +185,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic public UserCredentials requestPasswordReset(TenantId tenantId, String email) { log.trace("Executing requestPasswordReset email [{}]", email); validateString(email, "Incorrect email " + email); + DataValidator.validateEmail(email); User user = userDao.findByEmail(tenantId, email); if (user == null) { throw new IncorrectParameterException(String.format("Unable to find user by email [%s]", email)); @@ -278,7 +278,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic User user = findUserById(tenantId, userId); JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { - additionalInfo = objectMapper.createObjectNode(); + additionalInfo = JacksonUtil.newObjectNode(); } ((ObjectNode) additionalInfo).put(USER_CREDENTIALS_ENABLED, enabled); user.setAdditionalInfo(additionalInfo); @@ -301,7 +301,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private void setLastLoginTs(User user) { JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { - additionalInfo = objectMapper.createObjectNode(); + additionalInfo = JacksonUtil.newObjectNode(); } ((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis()); user.setAdditionalInfo(additionalInfo); @@ -310,7 +310,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private void resetFailedLoginAttempts(User user) { JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { - additionalInfo = objectMapper.createObjectNode(); + additionalInfo = JacksonUtil.newObjectNode(); } ((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0); user.setAdditionalInfo(additionalInfo); @@ -328,7 +328,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private int increaseFailedLoginAttempts(User user) { JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { - additionalInfo = objectMapper.createObjectNode(); + additionalInfo = JacksonUtil.newObjectNode(); } int failedLoginAttempts = 0; if (additionalInfo.has(FAILED_LOGIN_ATTEMPTS)) { @@ -352,26 +352,30 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic private void updatePasswordHistory(User user, UserCredentials userCredentials) { JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { - additionalInfo = objectMapper.createObjectNode(); + additionalInfo = JacksonUtil.newObjectNode(); } + Map userPasswordHistoryMap = null; + JsonNode userPasswordHistoryJson; if (additionalInfo.has(USER_PASSWORD_HISTORY)) { - JsonNode userPasswordHistoryJson = additionalInfo.get(USER_PASSWORD_HISTORY); - Map userPasswordHistoryMap = objectMapper.convertValue(userPasswordHistoryJson, Map.class); + userPasswordHistoryJson = additionalInfo.get(USER_PASSWORD_HISTORY); + userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>(){}); + } + if (userPasswordHistoryMap != null) { userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword()); - userPasswordHistoryJson = objectMapper.valueToTree(userPasswordHistoryMap); + userPasswordHistoryJson = JacksonUtil.valueToTree(userPasswordHistoryMap); ((ObjectNode) additionalInfo).replace(USER_PASSWORD_HISTORY, userPasswordHistoryJson); } else { - Map userPasswordHistoryMap = new HashMap<>(); + userPasswordHistoryMap = new HashMap<>(); userPasswordHistoryMap.put(Long.toString(System.currentTimeMillis()), userCredentials.getPassword()); - JsonNode userPasswordHistoryJson = objectMapper.valueToTree(userPasswordHistoryMap); + userPasswordHistoryJson = JacksonUtil.valueToTree(userPasswordHistoryMap); ((ObjectNode) additionalInfo).set(USER_PASSWORD_HISTORY, userPasswordHistoryJson); } user.setAdditionalInfo(additionalInfo); saveUser(user); } - private DataValidator userValidator = - new DataValidator() { + private final DataValidator userValidator = + new DataValidator<>() { @Override protected void validateCreate(TenantId tenantId, User user) { if (!user.getTenantId().getId().equals(ModelConstants.NULL_UUID)) { @@ -451,8 +455,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } }; - private DataValidator userCredentialsValidator = - new DataValidator() { + private final DataValidator userCredentialsValidator = + new DataValidator<>() { @Override protected void validateCreate(TenantId tenantId, UserCredentials userCredentials) { @@ -483,7 +487,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } }; - private PaginatedRemover tenantAdminsRemover = new PaginatedRemover() { + private final PaginatedRemover tenantAdminsRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { return userDao.findTenantAdmins(id.getId(), pageLink); @@ -495,7 +499,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } }; - private PaginatedRemover customerUsersRemover = new PaginatedRemover() { + private final PaginatedRemover customerUsersRemover = new PaginatedRemover<>() { @Override protected PageData findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) { return userDao.findCustomerUsers(tenantId.getId(), id.getId(), pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonTypeDescriptor.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonTypeDescriptor.java index 46a666a20c..6ba92c54c1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonTypeDescriptor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/JsonTypeDescriptor.java @@ -19,6 +19,7 @@ import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.AbstractTypeDescriptor; import org.hibernate.type.descriptor.java.MutableMutabilityPlan; import org.hibernate.usertype.DynamicParameterizedType; +import org.thingsboard.common.util.JacksonUtil; import java.util.Properties; diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 09dd42b622..787f457c6a 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS customer ( CREATE TABLE IF NOT EXISTS dashboard ( id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, created_time bigint NOT NULL, - configuration varchar, + configuration varchar(10000000), assigned_customers varchar(1000000), search_text varchar(255), tenant_id uuid, @@ -420,3 +420,11 @@ CREATE TABLE IF NOT EXISTS api_usage_state ( sms_exec varchar(32), CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) ); + +CREATE TABLE IF NOT EXISTS resource ( + tenant_id uuid NOT NULL, + resource_type varchar(32) NOT NULL, + resource_id varchar(255) NOT NULL, + resource_value varchar, + CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id) +); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 72e00ae85a..3bf5ce29c8 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -447,6 +447,14 @@ CREATE TABLE IF NOT EXISTS api_usage_state ( CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) ); +CREATE TABLE IF NOT EXISTS resource ( + tenant_id uuid NOT NULL, + resource_type varchar(32) NOT NULL, + resource_id varchar(255) NOT NULL, + resource_value varchar, + CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_id) +); + CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java b/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java index a5e6122537..69a164f729 100644 --- a/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java +++ b/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java @@ -244,6 +244,7 @@ public class Descriptor * * @return A Descriptor for the SSTable, and the Component remainder. */ + @SuppressWarnings("deprecation") public static Pair fromFilename(File directory, String name, boolean skipComponent) { File parentDirectory = directory != null ? directory : new File("."); @@ -319,6 +320,7 @@ public class Descriptor component); } + @SuppressWarnings("deprecation") public IMetadataSerializer getMetadataSerializer() { if (version.hasNewStatsFile()) diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java b/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java index af6af442a3..350d27591f 100644 --- a/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java +++ b/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java @@ -56,6 +56,7 @@ public interface SSTableFormat return BIG; } + @SuppressWarnings("deprecation") private Type(String name, SSTableFormat info) { //Since format comes right after generation diff --git a/dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java b/dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java new file mode 100644 index 0000000000..2609a2c3d0 --- /dev/null +++ b/dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java @@ -0,0 +1,760 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.io.util; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileStoreAttributeView; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.StreamSupport; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ScheduledExecutors; +import org.apache.cassandra.io.FSError; +import org.apache.cassandra.io.FSErrorHandler; +import org.apache.cassandra.io.FSReadError; +import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.utils.JVMStabilityInspector; + +import static com.google.common.base.Throwables.throwIfUnchecked; +import static org.apache.cassandra.utils.Throwables.maybeFail; +import static org.apache.cassandra.utils.Throwables.merge; + +public final class FileUtils +{ + public static final Charset CHARSET = StandardCharsets.UTF_8; + + private static final Logger logger = LoggerFactory.getLogger(FileUtils.class); + public static final long ONE_KB = 1024; + public static final long ONE_MB = 1024 * ONE_KB; + public static final long ONE_GB = 1024 * ONE_MB; + public static final long ONE_TB = 1024 * ONE_GB; + + private static final DecimalFormat df = new DecimalFormat("#.##"); + public static final boolean isCleanerAvailable = false; + private static final AtomicReference> fsErrorHandler = new AtomicReference<>(Optional.empty()); + + public static void createHardLink(String from, String to) + { + createHardLink(new File(from), new File(to)); + } + + public static void createHardLink(File from, File to) + { + if (to.exists()) + throw new RuntimeException("Tried to create duplicate hard link to " + to); + if (!from.exists()) + throw new RuntimeException("Tried to hard link to file that does not exist " + from); + + try + { + Files.createLink(to.toPath(), from.toPath()); + } + catch (IOException e) + { + throw new FSWriteError(e, to); + } + } + + public static File createTempFile(String prefix, String suffix, File directory) + { + try + { + return File.createTempFile(prefix, suffix, directory); + } + catch (IOException e) + { + throw new FSWriteError(e, directory); + } + } + + public static File createTempFile(String prefix, String suffix) + { + return createTempFile(prefix, suffix, new File(System.getProperty("java.io.tmpdir"))); + } + + public static Throwable deleteWithConfirm(String filePath, boolean expect, Throwable accumulate) + { + return deleteWithConfirm(new File(filePath), expect, accumulate); + } + + public static Throwable deleteWithConfirm(File file, boolean expect, Throwable accumulate) + { + boolean exists = file.exists(); + assert exists || !expect : "attempted to delete non-existing file " + file.getName(); + try + { + if (exists) + Files.delete(file.toPath()); + } + catch (Throwable t) + { + try + { + throw new FSWriteError(t, file); + } + catch (Throwable t2) + { + accumulate = merge(accumulate, t2); + } + } + return accumulate; + } + + public static void deleteWithConfirm(String file) + { + deleteWithConfirm(new File(file)); + } + + public static void deleteWithConfirm(File file) + { + maybeFail(deleteWithConfirm(file, true, null)); + } + + public static void renameWithOutConfirm(String from, String to) + { + try + { + atomicMoveWithFallback(new File(from).toPath(), new File(to).toPath()); + } + catch (IOException e) + { + if (logger.isTraceEnabled()) + logger.trace("Could not move file "+from+" to "+to, e); + } + } + + public static void renameWithConfirm(String from, String to) + { + renameWithConfirm(new File(from), new File(to)); + } + + public static void renameWithConfirm(File from, File to) + { + assert from.exists(); + if (logger.isTraceEnabled()) + logger.trace("Renaming {} to {}", from.getPath(), to.getPath()); + // this is not FSWE because usually when we see it it's because we didn't close the file before renaming it, + // and Windows is picky about that. + try + { + atomicMoveWithFallback(from.toPath(), to.toPath()); + } + catch (IOException e) + { + throw new RuntimeException(String.format("Failed to rename %s to %s", from.getPath(), to.getPath()), e); + } + } + + /** + * Move a file atomically, if it fails, it falls back to a non-atomic operation + * @param from + * @param to + * @throws IOException + */ + private static void atomicMoveWithFallback(Path from, Path to) throws IOException + { + try + { + Files.move(from, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } + catch (AtomicMoveNotSupportedException e) + { + logger.trace("Could not do an atomic move", e); + Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); + } + + } + public static void truncate(String path, long size) + { + try(FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ, StandardOpenOption.WRITE)) + { + channel.truncate(size); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + public static void closeQuietly(Closeable c) + { + try + { + if (c != null) + c.close(); + } + catch (Exception e) + { + logger.warn("Failed closing {}", c, e); + } + } + + public static void closeQuietly(AutoCloseable c) + { + try + { + if (c != null) + c.close(); + } + catch (Exception e) + { + logger.warn("Failed closing {}", c, e); + } + } + + public static void close(Closeable... cs) throws IOException + { + close(Arrays.asList(cs)); + } + + public static void close(Iterable cs) throws IOException + { + Throwable e = null; + for (Closeable c : cs) + { + try + { + if (c != null) + c.close(); + } + catch (Throwable ex) + { + if (e == null) e = ex; + else e.addSuppressed(ex); + logger.warn("Failed closing stream {}", c, ex); + } + } + maybeFail(e, IOException.class); + } + + public static void closeQuietly(Iterable cs) + { + for (AutoCloseable c : cs) + { + try + { + if (c != null) + c.close(); + } + catch (Exception ex) + { + logger.warn("Failed closing {}", c, ex); + } + } + } + + public static String getCanonicalPath(String filename) + { + try + { + return new File(filename).getCanonicalPath(); + } + catch (IOException e) + { + throw new FSReadError(e, filename); + } + } + + public static String getCanonicalPath(File file) + { + try + { + return file.getCanonicalPath(); + } + catch (IOException e) + { + throw new FSReadError(e, file); + } + } + + /** Return true if file is contained in folder */ + public static boolean isContained(File folder, File file) + { + Path folderPath = Paths.get(getCanonicalPath(folder)); + Path filePath = Paths.get(getCanonicalPath(file)); + + return filePath.startsWith(folderPath); + } + + /** Convert absolute path into a path relative to the base path */ + public static String getRelativePath(String basePath, String path) + { + try + { + return Paths.get(basePath).relativize(Paths.get(path)).toString(); + } + catch(Exception ex) + { + String absDataPath = FileUtils.getCanonicalPath(basePath); + return Paths.get(absDataPath).relativize(Paths.get(path)).toString(); + } + } + + public static void clean(ByteBuffer buffer) + { + if (buffer == null) + return; + } + + public static void createDirectory(String directory) + { + createDirectory(new File(directory)); + } + + public static void createDirectory(File directory) + { + if (!directory.exists()) + { + if (!directory.mkdirs()) + throw new FSWriteError(new IOException("Failed to mkdirs " + directory), directory); + } + } + + public static boolean delete(String file) + { + File f = new File(file); + return f.delete(); + } + + public static void delete(File... files) + { + if (files == null) + { + // CASSANDRA-13389: some callers use Files.listFiles() which, on error, silently returns null + logger.debug("Received null list of files to delete"); + return; + } + + for ( File file : files ) + { + file.delete(); + } + } + + public static void deleteAsync(final String file) + { + Runnable runnable = new Runnable() + { + public void run() + { + deleteWithConfirm(new File(file)); + } + }; + ScheduledExecutors.nonPeriodicTasks.execute(runnable); + } + + public static void visitDirectory(Path dir, Predicate filter, Consumer consumer) + { + try (DirectoryStream stream = Files.newDirectoryStream(dir)) + { + StreamSupport.stream(stream.spliterator(), false) + .map(Path::toFile) + // stream directories are weakly consistent so we always check if the file still exists + .filter(f -> f.exists() && (filter == null || filter.test(f))) + .forEach(consumer); + } + catch (IOException|DirectoryIteratorException ex) + { + logger.error("Failed to list files in {} with exception: {}", dir, ex.getMessage(), ex); + } + } + + public static String stringifyFileSize(double value) + { + double d; + if ( value >= ONE_TB ) + { + d = value / ONE_TB; + String val = df.format(d); + return val + " TiB"; + } + else if ( value >= ONE_GB ) + { + d = value / ONE_GB; + String val = df.format(d); + return val + " GiB"; + } + else if ( value >= ONE_MB ) + { + d = value / ONE_MB; + String val = df.format(d); + return val + " MiB"; + } + else if ( value >= ONE_KB ) + { + d = value / ONE_KB; + String val = df.format(d); + return val + " KiB"; + } + else + { + String val = df.format(value); + return val + " bytes"; + } + } + + /** + * Deletes all files and subdirectories under "dir". + * @param dir Directory to be deleted + * @throws FSWriteError if any part of the tree cannot be deleted + */ + public static void deleteRecursive(File dir) + { + if (dir.isDirectory()) + { + String[] children = dir.list(); + for (String child : children) + deleteRecursive(new File(dir, child)); + } + + // The directory is now empty so now it can be smoked + deleteWithConfirm(dir); + } + + /** + * Schedules deletion of all file and subdirectories under "dir" on JVM shutdown. + * @param dir Directory to be deleted + */ + public static void deleteRecursiveOnExit(File dir) + { + if (dir.isDirectory()) + { + String[] children = dir.list(); + for (String child : children) + deleteRecursiveOnExit(new File(dir, child)); + } + + logger.trace("Scheduling deferred deletion of file: {}", dir); + dir.deleteOnExit(); + } + + public static void handleCorruptSSTable(CorruptSSTableException e) + { + fsErrorHandler.get().ifPresent(handler -> handler.handleCorruptSSTable(e)); + } + + public static void handleFSError(FSError e) + { + fsErrorHandler.get().ifPresent(handler -> handler.handleFSError(e)); + } + + /** + * handleFSErrorAndPropagate will invoke the disk failure policy error handler, + * which may or may not stop the daemon or transports. However, if we don't exit, + * we still want to propagate the exception to the caller in case they have custom + * exception handling + * + * @param e A filesystem error + */ + public static void handleFSErrorAndPropagate(FSError e) + { + JVMStabilityInspector.inspectThrowable(e); + throwIfUnchecked(e); + throw new RuntimeException(e); + } + + /** + * Get the size of a directory in bytes + * @param folder The directory for which we need size. + * @return The size of the directory + */ + public static long folderSize(File folder) + { + final long [] sizeArr = {0L}; + try + { + Files.walkFileTree(folder.toPath(), new SimpleFileVisitor() + { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + { + sizeArr[0] += attrs.size(); + return FileVisitResult.CONTINUE; + } + }); + } + catch (IOException e) + { + logger.error("Error while getting {} folder size. {}", folder, e); + } + return sizeArr[0]; + } + + public static void copyTo(DataInput in, OutputStream out, int length) throws IOException + { + byte[] buffer = new byte[64 * 1024]; + int copiedBytes = 0; + + while (copiedBytes + buffer.length < length) + { + in.readFully(buffer); + out.write(buffer); + copiedBytes += buffer.length; + } + + if (copiedBytes < length) + { + int left = length - copiedBytes; + in.readFully(buffer, 0, left); + out.write(buffer, 0, left); + } + } + + public static boolean isSubDirectory(File parent, File child) throws IOException + { + parent = parent.getCanonicalFile(); + child = child.getCanonicalFile(); + + File toCheck = child; + while (toCheck != null) + { + if (parent.equals(toCheck)) + return true; + toCheck = toCheck.getParentFile(); + } + return false; + } + + public static void append(File file, String ... lines) + { + if (file.exists()) + write(file, Arrays.asList(lines), StandardOpenOption.APPEND); + else + write(file, Arrays.asList(lines), StandardOpenOption.CREATE); + } + + public static void appendAndSync(File file, String ... lines) + { + if (file.exists()) + write(file, Arrays.asList(lines), StandardOpenOption.APPEND, StandardOpenOption.SYNC); + else + write(file, Arrays.asList(lines), StandardOpenOption.CREATE, StandardOpenOption.SYNC); + } + + public static void replace(File file, String ... lines) + { + write(file, Arrays.asList(lines), StandardOpenOption.TRUNCATE_EXISTING); + } + + public static void write(File file, List lines, StandardOpenOption ... options) + { + try + { + Files.write(file.toPath(), + lines, + CHARSET, + options); + } + catch (IOException ex) + { + throw new RuntimeException(ex); + } + } + + public static List readLines(File file) + { + try + { + return Files.readAllLines(file.toPath(), CHARSET); + } + catch (IOException ex) + { + if (ex instanceof NoSuchFileException) + return Collections.emptyList(); + + throw new RuntimeException(ex); + } + } + + public static void setFSErrorHandler(FSErrorHandler handler) + { + fsErrorHandler.getAndSet(Optional.ofNullable(handler)); + } + + /** + * Returns the size of the specified partition. + *

This method handles large file system by returning {@code Long.MAX_VALUE} if the size overflow. + * See JDK-8179320 for more information.

+ * + * @param file the partition + * @return the size, in bytes, of the partition or {@code 0L} if the abstract pathname does not name a partition + */ + public static long getTotalSpace(File file) + { + return handleLargeFileSystem(file.getTotalSpace()); + } + + /** + * Returns the number of unallocated bytes on the specified partition. + *

This method handles large file system by returning {@code Long.MAX_VALUE} if the number of unallocated bytes + * overflow. See JDK-8179320 for more information

+ * + * @param file the partition + * @return the number of unallocated bytes on the partition or {@code 0L} + * if the abstract pathname does not name a partition. + */ + public static long getFreeSpace(File file) + { + return handleLargeFileSystem(file.getFreeSpace()); + } + + /** + * Returns the number of available bytes on the specified partition. + *

This method handles large file system by returning {@code Long.MAX_VALUE} if the number of available bytes + * overflow. See JDK-8179320 for more information

+ * + * @param file the partition + * @return the number of available bytes on the partition or {@code 0L} + * if the abstract pathname does not name a partition. + */ + public static long getUsableSpace(File file) + { + return handleLargeFileSystem(file.getUsableSpace()); + } + + /** + * Returns the {@link FileStore} representing the file store where a file + * is located. This {@link FileStore} handles large file system by returning {@code Long.MAX_VALUE} + * from {@code FileStore#getTotalSpace()}, {@code FileStore#getUnallocatedSpace()} and {@code FileStore#getUsableSpace()} + * it the value is bigger than {@code Long.MAX_VALUE}. See JDK-8162520 + * for more information. + * + * @param path the path to the file + * @return the file store where the file is stored + */ + public static FileStore getFileStore(Path path) throws IOException + { + return new SafeFileStore(Files.getFileStore(path)); + } + + /** + * Handle large file system by returning {@code Long.MAX_VALUE} when the size overflows. + * @param size returned by the Java's FileStore methods + * @return the size or {@code Long.MAX_VALUE} if the size was bigger than {@code Long.MAX_VALUE} + */ + private static long handleLargeFileSystem(long size) + { + return size < 0 ? Long.MAX_VALUE : size; + } + + /** + * Private constructor as the class contains only static methods. + */ + private FileUtils() + { + } + + /** + * FileStore decorator used to safely handle large file system. + * + *

Java's FileStore methods (getTotalSpace/getUnallocatedSpace/getUsableSpace) are limited to reporting bytes as + * signed long (2^63-1), if the filesystem is any bigger, then the size overflows. {@code SafeFileStore} will + * return {@code Long.MAX_VALUE} if the size overflow.

+ * + * @see https://bugs.openjdk.java.net/browse/JDK-8162520. + */ + private static final class SafeFileStore extends FileStore + { + /** + * The decorated {@code FileStore} + */ + private final FileStore fileStore; + + public SafeFileStore(FileStore fileStore) + { + this.fileStore = fileStore; + } + + @Override + public String name() + { + return fileStore.name(); + } + + @Override + public String type() + { + return fileStore.type(); + } + + @Override + public boolean isReadOnly() + { + return fileStore.isReadOnly(); + } + + @Override + public long getTotalSpace() throws IOException + { + return handleLargeFileSystem(fileStore.getTotalSpace()); + } + + @Override + public long getUsableSpace() throws IOException + { + return handleLargeFileSystem(fileStore.getUsableSpace()); + } + + @Override + public long getUnallocatedSpace() throws IOException + { + return handleLargeFileSystem(fileStore.getUnallocatedSpace()); + } + + @Override + public boolean supportsFileAttributeView(Class type) + { + return fileStore.supportsFileAttributeView(type); + } + + @Override + public boolean supportsFileAttributeView(String name) + { + return fileStore.supportsFileAttributeView(name); + } + + @Override + public V getFileStoreAttributeView(Class type) + { + return fileStore.getFileStoreAttributeView(type); + } + + @Override + public Object getAttribute(String attribute) throws IOException + { + return fileStore.getAttribute(attribute); + } + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java index 5530bdbcb2..d3c6c97367 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java @@ -25,7 +25,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.env.Environment; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.id.TenantId; @@ -35,9 +35,9 @@ import org.thingsboard.server.dao.timeseries.CassandraBaseTimeseriesDao; import java.util.UUID; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index 3536b6066f..a798133ad4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.alarm.AlarmOperationResult; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import java.util.Arrays; import java.util.Collections; @@ -347,13 +347,69 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { } private AlarmDataQuery toQuery(AlarmDataPageLink pageLink){ - return toQuery(pageLink, Collections.EMPTY_LIST); + return toQuery(pageLink, Collections.emptyList()); } private AlarmDataQuery toQuery(AlarmDataPageLink pageLink, List alarmFields){ return new AlarmDataQuery(new DeviceTypeFilter(), pageLink, null, null, null, alarmFields); } + @Test + public void testFindHighestAlarmSeverity() throws ExecutionException, InterruptedException { + Customer customer = new Customer(); + customer.setTitle("TestCustomer"); + customer.setTenantId(tenantId); + customer = customerService.saveCustomer(customer); + + Device customerDevice = new Device(); + customerDevice.setName("TestCustomerDevice"); + customerDevice.setType("default"); + customerDevice.setTenantId(tenantId); + customerDevice.setCustomerId(customer.getId()); + customerDevice = deviceService.saveDevice(customerDevice); + + // no one alarms was created + Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); + + Alarm alarm1 = Alarm.builder() + .tenantId(tenantId) + .originator(customerDevice.getId()) + .type(TEST_ALARM) + .severity(AlarmSeverity.MAJOR) + .status(AlarmStatus.ACTIVE_UNACK) + .startTs(System.currentTimeMillis()) + .build(); + alarm1 = alarmService.createOrUpdateAlarm(alarm1).getAlarm(); + alarmService.clearAlarm(tenantId, alarm1.getId(), null, System.currentTimeMillis()).get(); + + Alarm alarm2 = Alarm.builder() + .tenantId(tenantId) + .originator(customerDevice.getId()) + .type(TEST_ALARM) + .severity(AlarmSeverity.MINOR) + .status(AlarmStatus.ACTIVE_ACK) + .startTs(System.currentTimeMillis()) + .build(); + alarm2 = alarmService.createOrUpdateAlarm(alarm2).getAlarm(); + alarmService.clearAlarm(tenantId, alarm2.getId(), null, System.currentTimeMillis()).get(); + + Alarm alarm3 = Alarm.builder() + .tenantId(tenantId) + .originator(customerDevice.getId()) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL) + .status(AlarmStatus.ACTIVE_ACK) + .startTs(System.currentTimeMillis()) + .build(); + alarm3 = alarmService.createOrUpdateAlarm(alarm3).getAlarm(); + + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null)); + Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK)); + } + @Test public void testFindAlarmUsingAlarmDataQuery() throws ExecutionException, InterruptedException { AssetId parentId = new AssetId(Uuids.timeBased()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 70d97a8726..6c899c01c4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -15,9 +15,6 @@ */ package org.thingsboard.server.dao.service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.junit.After; @@ -31,25 +28,48 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.id.*; -import org.thingsboard.server.common.data.kv.*; +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.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.query.*; +import org.thingsboard.server.common.data.query.AssetSearchQueryFilter; +import org.thingsboard.server.common.data.query.DeviceSearchQueryFilter; +import org.thingsboard.server.common.data.query.DeviceTypeFilter; +import org.thingsboard.server.common.data.query.EntityCountQuery; +import org.thingsboard.server.common.data.query.EntityData; +import org.thingsboard.server.common.data.query.EntityDataPageLink; +import org.thingsboard.server.common.data.query.EntityDataQuery; +import org.thingsboard.server.common.data.query.EntityDataSortOrder; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.EntityListFilter; +import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.query.KeyFilter; +import org.thingsboard.server.common.data.query.NumericFilterPredicate; +import org.thingsboard.server.common.data.query.RelationsQueryFilter; +import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainMetaData; -import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; -import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.timeseries.TimeseriesService; -import org.thingsboard.server.dao.util.DaoTestUtil; -import org.thingsboard.server.dao.util.SqlDbType; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -140,13 +160,13 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(30, count); - filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(25, count); filter.setRootEntity(devices.get(0).getId()); filter.setDirection(EntitySearchDirection.TO); - filter.setFilters(Collections.singletonList(new EntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Manages", Collections.singletonList(EntityType.TENANT)))); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); Assert.assertEquals(1, count); @@ -208,7 +228,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { RelationsQueryFilter filter = new RelationsQueryFilter(); filter.setRootEntity(tenantId); filter.setDirection(EntitySearchDirection.FROM); - filter.setFilters(Collections.singletonList(new EntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); + filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); EntityDataSortOrder sortOrder = new EntityDataSortOrder( new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC @@ -827,10 +847,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { .getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); } List deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); - if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) { - // in H2 double values are stored with E0 in the end of the string - loadedTemperatures = loadedTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList()); - } + Assert.assertEquals(deviceTemperatures, loadedTemperatures); pageLink = new EntityDataPageLink(10, 0, null, sortOrder); @@ -858,10 +875,6 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList()); List deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList()); - if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) { - // in H2 double values are stored with E0 in the end of the string - loadedHighTemperatures = loadedHighTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList()); - } Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures); deviceService.deleteDevicesByTenantId(tenantId); @@ -1200,7 +1213,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, sortOrder); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, deviceTypeFilters); - PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); + PageData data = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query); List loadedEntities = getLoadedEntities(data, query); Assert.assertEquals(devices.size(), loadedEntities.size()); @@ -1227,7 +1240,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { return A.containsAll(B) && B.containsAll(A); } - private List getLoadedEntities(PageData data, EntityDataQuery query) { + private List getLoadedEntities(PageData data, EntityDataQuery query) { List loadedEntities = new ArrayList<>(data.getData()); while (data.hasNext()) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java index 84c0a2975c..6f5ea9acb9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.exception.DataValidationException; @@ -221,7 +221,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); - query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); List relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(3, relations.size()); Assert.assertTrue(relations.contains(relationA)); @@ -255,7 +255,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { EntityRelationsQuery query = new EntityRelationsQuery(); query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false)); - query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); + query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); List relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(2, relations.size()); Assert.assertTrue(relations.contains(relationAB)); diff --git a/dao/src/test/resources/nosql-test.properties b/dao/src/test/resources/nosql-test.properties index eefa4f0b95..6fe2dc2112 100644 --- a/dao/src/test/resources/nosql-test.properties +++ b/dao/src/test/resources/nosql-test.properties @@ -6,6 +6,8 @@ sql.ts_inserts_fixed_thread_pool_size=10 spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.properties.hibernate.order_by.default_null_ordering=last +spring.jpa.properties.hibernate.jdbc.log.warnings=false + spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=none spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect diff --git a/dao/src/test/resources/sql-test.properties b/dao/src/test/resources/sql-test.properties index 058d7ac056..358b9ea3af 100644 --- a/dao/src/test/resources/sql-test.properties +++ b/dao/src/test/resources/sql-test.properties @@ -7,6 +7,8 @@ sql.ts_key_value_partitioning=MONTHS # spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.properties.hibernate.order_by.default_null_ordering=last +spring.jpa.properties.hibernate.jdbc.log.warnings=false + spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=validate spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect @@ -49,4 +51,4 @@ queue.rule-engine.queues[0].pack-processing-timeout=3000 queue.rule-engine.queues[0].processing-strategy.type=SKIP_ALL_FAILURES queue.rule-engine.queues[0].submit-strategy.type=BURST -sql.log_entity_queries=true \ No newline at end of file +sql.log_entity_queries=true diff --git a/dao/src/test/resources/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index b5629b0560..a548ceec30 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -28,4 +28,5 @@ DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; DROP TABLE IF EXISTS api_usage_state; +DROP TABLE IF EXISTS resource; DROP FUNCTION IF EXISTS to_uuid; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 34985883d7..333ce03fba 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -28,4 +28,5 @@ DROP TABLE IF EXISTS tb_schema_settings; DROP TABLE IF EXISTS oauth2_client_registration; DROP TABLE IF EXISTS oauth2_client_registration_info; DROP TABLE IF EXISTS oauth2_client_registration_template; -DROP TABLE IF EXISTS api_usage_state; \ No newline at end of file +DROP TABLE IF EXISTS api_usage_state; +DROP TABLE IF EXISTS resource; diff --git a/docker/.env b/docker/.env index b632300d5f..f7b30aafe0 100644 --- a/docker/.env +++ b/docker/.env @@ -8,6 +8,7 @@ WEB_UI_DOCKER_NAME=tb-web-ui MQTT_TRANSPORT_DOCKER_NAME=tb-mqtt-transport HTTP_TRANSPORT_DOCKER_NAME=tb-http-transport COAP_TRANSPORT_DOCKER_NAME=tb-coap-transport +LWM2M_TRANSPORT_DOCKER_NAME=tb-lwm2m-transport TB_VERSION=latest diff --git a/docker/docker-compose.aws-sqs.yml b/docker/docker-compose.aws-sqs.yml index 6966c4038b..f70ad9410c 100644 --- a/docker/docker-compose.aws-sqs.yml +++ b/docker/docker-compose.aws-sqs.yml @@ -68,4 +68,9 @@ services: env_file: - queue-aws-sqs.env depends_on: - - zookeeper \ No newline at end of file + - zookeeper + tb-lwm2m-transport: + env_file: + - queue-aws-sqs.env + depends_on: + - zookeeper diff --git a/docker/docker-compose.confluent.yml b/docker/docker-compose.confluent.yml index d4b22a6052..e048ed0524 100644 --- a/docker/docker-compose.confluent.yml +++ b/docker/docker-compose.confluent.yml @@ -55,3 +55,6 @@ services: tb-coap-transport: env_file: - queue-confluent.env + tb-lwm2m-transport: + env_file: + - queue-confluent.env diff --git a/docker/docker-compose.kafka.yml b/docker/docker-compose.kafka.yml index 29c704e0f0..e46df318c5 100644 --- a/docker/docker-compose.kafka.yml +++ b/docker/docker-compose.kafka.yml @@ -80,3 +80,8 @@ services: - queue-kafka.env depends_on: - kafka + tb-lwm2m-transport: + env_file: + - queue-kafka.env + depends_on: + - kafka diff --git a/docker/docker-compose.postgres.volumes.yml b/docker/docker-compose.postgres.volumes.yml index 679fd82435..2819bdf62d 100644 --- a/docker/docker-compose.postgres.volumes.yml +++ b/docker/docker-compose.postgres.volumes.yml @@ -35,6 +35,9 @@ services: tb-coap-transport: volumes: - tb-coap-transport-log-volume:/var/log/tb-coap-transport + tb-lwm2m-transport: + volumes: + - tb-lwm2m-transport-log-volume:/var/log/tb-lwm2m-transport tb-http-transport1: volumes: - tb-http-transport-log-volume:/var/log/tb-http-transport @@ -58,6 +61,9 @@ volumes: tb-coap-transport-log-volume: external: true name: ${TB_COAP_TRANSPORT_LOG_VOLUME} + tb-lwm2m-transport-log-volume: + external: true + name: ${TB_LWM2M_TRANSPORT_LOG_VOLUME} tb-http-transport-log-volume: external: true name: ${TB_HTTP_TRANSPORT_LOG_VOLUME} diff --git a/docker/docker-compose.pubsub.yml b/docker/docker-compose.pubsub.yml index 5213ed451a..3041768951 100644 --- a/docker/docker-compose.pubsub.yml +++ b/docker/docker-compose.pubsub.yml @@ -69,3 +69,8 @@ services: - queue-pubsub.env depends_on: - zookeeper + tb-lwm2m-transport: + env_file: + - queue-pubsub.env + depends_on: + - zookeeper diff --git a/docker/docker-compose.rabbitmq.yml b/docker/docker-compose.rabbitmq.yml index 2caacf193e..b4d4b6cf3e 100644 --- a/docker/docker-compose.rabbitmq.yml +++ b/docker/docker-compose.rabbitmq.yml @@ -68,4 +68,9 @@ services: env_file: - queue-rabbitmq.env depends_on: - - zookeeper \ No newline at end of file + - zookeeper + tb-lwm2m-transport: + env_file: + - queue-rabbitmq.env + depends_on: + - zookeeper diff --git a/docker/docker-compose.service-bus.yml b/docker/docker-compose.service-bus.yml index 9dac4e7d13..29004d61fa 100644 --- a/docker/docker-compose.service-bus.yml +++ b/docker/docker-compose.service-bus.yml @@ -67,5 +67,8 @@ services: tb-coap-transport: env_file: - queue-service-bus.env + tb-lwm2m-transport: + env_file: + - queue-service-bus.env depends_on: - - zookeeper \ No newline at end of file + - zookeeper diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a051a98742..b71a5c097b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -199,6 +199,20 @@ services: - ./tb-transports/coap/log:/var/log/tb-coap-transport depends_on: - zookeeper + tb-lwm2m-transport: + restart: always + image: "${DOCKER_REPO}/${LWM2M_TRANSPORT_DOCKER_NAME}:${TB_VERSION}" + ports: + - "5685:5685/udp" + environment: + TB_SERVICE_ID: tb-lwm2m-transport + env_file: + - tb-lwm2m-transport.env + volumes: + - ./tb-transports/lwm2m/conf:/config + - ./tb-transports/lwm2m/log:/var/log/tb-lwm2m-transport + depends_on: + - zookeeper tb-web-ui1: restart: always image: "${DOCKER_REPO}/${WEB_UI_DOCKER_NAME}:${TB_VERSION}" diff --git a/docker/docker-create-log-folders.sh b/docker/docker-create-log-folders.sh index c877ba1460..3ef3292f22 100755 --- a/docker/docker-create-log-folders.sh +++ b/docker/docker-create-log-folders.sh @@ -19,6 +19,8 @@ mkdir -p tb-node/log/ && sudo chown -R 799:799 tb-node/log/ mkdir -p tb-transports/coap/log && sudo chown -R 799:799 tb-transports/coap/log +mkdir -p tb-transports/lwm2m/log && sudo chown -R 799:799 tb-transports/lwm2m/log + mkdir -p tb-transports/http/log && sudo chown -R 799:799 tb-transports/http/log -mkdir -p tb-transports/mqtt/log && sudo chown -R 799:799 tb-transports/mqtt/log \ No newline at end of file +mkdir -p tb-transports/mqtt/log && sudo chown -R 799:799 tb-transports/mqtt/log diff --git a/docker/tb-lwm2m-transport.env b/docker/tb-lwm2m-transport.env new file mode 100644 index 0000000000..6e6cad922b --- /dev/null +++ b/docker/tb-lwm2m-transport.env @@ -0,0 +1,6 @@ +ZOOKEEPER_ENABLED=true +ZOOKEEPER_URL=zookeeper:2181 + +LWM2M_BIND_ADDRESS=0.0.0.0 +LWM2M_BIND_PORT=5685 +LWM2M_TIMEOUT=10000 diff --git a/docker/tb-node/conf/thingsboard.conf b/docker/tb-node/conf/thingsboard.conf index 3f27bf2859..ead283da70 100644 --- a/docker/tb-node/conf/thingsboard.conf +++ b/docker/tb-node/conf/thingsboard.conf @@ -15,10 +15,10 @@ # export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data" -export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/thingsboard/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/thingsboard/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_SERVICE_ID}-heapdump.bin" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=thingsboard.out export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions diff --git a/docker/tb-transports/coap/conf/tb-coap-transport.conf b/docker/tb-transports/coap/conf/tb-coap-transport.conf index 7c5f134183..2db8f579fc 100644 --- a/docker/tb-transports/coap/conf/tb-coap-transport.conf +++ b/docker/tb-transports/coap/conf/tb-coap-transport.conf @@ -14,10 +14,10 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-coap-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-coap-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_SERVICE_ID}-heapdump.bin" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-coap-transport.out export LOADER_PATH=/usr/share/tb-coap-transport/conf diff --git a/docker/tb-transports/http/conf/tb-http-transport.conf b/docker/tb-transports/http/conf/tb-http-transport.conf index 795f5424db..b61070a3ff 100644 --- a/docker/tb-transports/http/conf/tb-http-transport.conf +++ b/docker/tb-transports/http/conf/tb-http-transport.conf @@ -14,10 +14,10 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-http-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-http-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_SERVICE_ID}-heapdump.bin" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-http-transport.out export LOADER_PATH=/usr/share/tb-http-transport/conf diff --git a/docker/tb-transports/lwm2m/conf/logback.xml b/docker/tb-transports/lwm2m/conf/logback.xml new file mode 100644 index 0000000000..ad1e85fac8 --- /dev/null +++ b/docker/tb-transports/lwm2m/conf/logback.xml @@ -0,0 +1,50 @@ + + + + + + + /var/log/tb-lwm2m-transport/${TB_SERVICE_ID}/tb-lwm2m-transport.log + + /var/log/tb-lwm2m-transport/${TB_SERVICE_ID}/tb-lwm2m-transport.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/docker/tb-transports/lwm2m/conf/tb-lwm2m-transport.conf b/docker/tb-transports/lwm2m/conf/tb-lwm2m-transport.conf new file mode 100644 index 0000000000..2d28a77174 --- /dev/null +++ b/docker/tb-transports/lwm2m/conf/tb-lwm2m-transport.conf @@ -0,0 +1,23 @@ +# +# Copyright © 2016-2021 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. +# + +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-lwm2m-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-lwm2m-transport/${TB_SERVICE_ID}-heapdump.bin" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" +export LOG_FILENAME=tb-lwm2m-transport.out +export LOADER_PATH=/usr/share/tb-lwm2m-transport/conf diff --git a/docker/tb-transports/mqtt/conf/tb-mqtt-transport.conf b/docker/tb-transports/mqtt/conf/tb-mqtt-transport.conf index cc23a7af65..5c0b66ba8e 100644 --- a/docker/tb-transports/mqtt/conf/tb-mqtt-transport.conf +++ b/docker/tb-transports/mqtt/conf/tb-mqtt-transport.conf @@ -14,10 +14,10 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}/heapdump.bin -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-heapdump.bin" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-mqtt-transport.out export LOADER_PATH=/usr/share/tb-mqtt-transport/conf diff --git a/k8s/common/thingsboard.yml b/k8s/common/thingsboard.yml index afc5f0808b..51f39823df 100644 --- a/k8s/common/thingsboard.yml +++ b/k8s/common/thingsboard.yml @@ -265,6 +265,72 @@ spec: --- apiVersion: apps/v1 kind: Deployment +metadata: + name: tb-lwm2m-transport + namespace: thingsboard +spec: + replicas: 2 + selector: + matchLabels: + app: tb-lwm2m-transport + template: + metadata: + labels: + app: tb-lwm2m-transport + spec: + volumes: + - name: tb-lwm2m-transport-config + configMap: + name: tb-lwm2m-transport-config + items: + - key: conf + path: tb-lwm2m-transport.conf + - key: logback + path: logback.xml + containers: + - name: server + imagePullPolicy: Always + image: thingsboard/tb-lwm2m-transport:latest + ports: + - containerPort: 5685 + name: lwm2m + protocol: UDP + env: + - name: TB_SERVICE_ID + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: TB_QUEUE_TYPE + value: "kafka" + - name: LWM2M_BIND_ADDRESS + value: "0.0.0.0" + - name: LWM2M_BIND_PORT + value: "5685" + - name: LWM2M_TIMEOUT + value: "10000" + - name: TB_KAFKA_SERVERS + value: "tb-kafka:9092" + volumeMounts: + - mountPath: /config + name: tb-lwm2m-transport-config + restartPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + name: tb-lwm2m-transport + namespace: thingsboard +spec: + type: LoadBalancer + selector: + app: tb-lwm2m-transport + ports: + - port: 5685 + name: lwm2m + protocol: UDP +--- +apiVersion: apps/v1 +kind: Deployment metadata: name: tb-web-ui namespace: thingsboard diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000000..d904701090 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +config.stopbubbling = true +lombok.anyconstructor.addconstructorproperties = true diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 9cfaea0ed9..9af00d44c7 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index e17e5d0261..412cf3094b 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; @@ -114,14 +115,17 @@ public abstract class AbstractContainerTest { } protected Device createDevice(String name) { - return restClient.createDevice(name + RandomStringUtils.randomAlphanumeric(7), "DEFAULT"); + Device device = new Device(); + device.setName(name + RandomStringUtils.randomAlphanumeric(7)); + device.setType("DEFAULT"); + return restClient.saveDevice(device); } protected WsClient subscribeToWebSocket(DeviceId deviceId, String scope, CmdsType property) throws Exception { WsClient wsClient = new WsClient(new URI(WSS_URL + "/api/ws/plugins/telemetry?token=" + restClient.getToken())); SSLContextBuilder builder = SSLContexts.custom(); builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); - wsClient.setSocket(builder.build().getSocketFactory().createSocket()); + wsClient.setSocketFactory(builder.build().getSocketFactory()); wsClient.connectBlocking(); JsonObject cmdsObject = new JsonObject(); @@ -218,24 +222,7 @@ public abstract class AbstractContainerTest { SSLContextBuilder builder = SSLContexts.custom(); builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true); SSLContext sslContext = builder.build(); - SSLConnectionSocketFactory sslSelfSigned = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() { - @Override - public void verify(String host, SSLSocket ssl) { - } - - @Override - public void verify(String host, X509Certificate cert) { - } - - @Override - public void verify(String host, String[] cns, String[] subjectAlts) { - } - - @Override - public boolean verify(String s, SSLSession sslSession) { - return true; - } - }); + SSLConnectionSocketFactory sslSelfSigned = new SSLConnectionSocketFactory(sslContext, (s, sslSession) -> true); Registry socketFactoryRegistry = RegistryBuilder .create() diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java index 78e94d001c..acaf417882 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java @@ -34,7 +34,7 @@ import java.util.Map; @ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"}) public class ContainerTestSuite { - private static DockerComposeContainer testContainer; + private static DockerComposeContainer testContainer; @ClassRule public static ThingsBoardDbInstaller installTb = new ThingsBoardDbInstaller(); @@ -43,7 +43,7 @@ public class ContainerTestSuite { public static DockerComposeContainer getTestContainer() { if (testContainer == null) { boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers")); - testContainer = new DockerComposeContainer( + testContainer = new DockerComposeContainer<>( new File("./../../docker/docker-compose.yml"), new File("./../../docker/docker-compose.postgres.yml"), new File("./../../docker/docker-compose.postgres.volumes.yml"), diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index 9189b74e26..3b1871b12b 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -29,6 +29,7 @@ public class ThingsBoardDbInstaller extends ExternalResource { private final static String POSTGRES_DATA_VOLUME = "tb-postgres-test-data-volume"; private final static String TB_LOG_VOLUME = "tb-log-test-volume"; private final static String TB_COAP_TRANSPORT_LOG_VOLUME = "tb-coap-transport-log-test-volume"; + private final static String TB_LWM2M_TRANSPORT_LOG_VOLUME = "tb-lwm2m-transport-log-test-volume"; private final static String TB_HTTP_TRANSPORT_LOG_VOLUME = "tb-http-transport-log-test-volume"; private final static String TB_MQTT_TRANSPORT_LOG_VOLUME = "tb-mqtt-transport-log-test-volume"; @@ -37,6 +38,7 @@ public class ThingsBoardDbInstaller extends ExternalResource { private final String postgresDataVolume; private final String tbLogVolume; private final String tbCoapTransportLogVolume; + private final String tbLwm2mTransportLogVolume; private final String tbHttpTransportLogVolume; private final String tbMqttTransportLogVolume; private final Map env; @@ -52,6 +54,7 @@ public class ThingsBoardDbInstaller extends ExternalResource { postgresDataVolume = project + "_" + POSTGRES_DATA_VOLUME; tbLogVolume = project + "_" + TB_LOG_VOLUME; tbCoapTransportLogVolume = project + "_" + TB_COAP_TRANSPORT_LOG_VOLUME; + tbLwm2mTransportLogVolume = project + "_" + TB_LWM2M_TRANSPORT_LOG_VOLUME; tbHttpTransportLogVolume = project + "_" + TB_HTTP_TRANSPORT_LOG_VOLUME; tbMqttTransportLogVolume = project + "_" + TB_MQTT_TRANSPORT_LOG_VOLUME; @@ -61,6 +64,7 @@ public class ThingsBoardDbInstaller extends ExternalResource { env.put("POSTGRES_DATA_VOLUME", postgresDataVolume); env.put("TB_LOG_VOLUME", tbLogVolume); env.put("TB_COAP_TRANSPORT_LOG_VOLUME", tbCoapTransportLogVolume); + env.put("TB_LWM2M_TRANSPORT_LOG_VOLUME", tbLwm2mTransportLogVolume); env.put("TB_HTTP_TRANSPORT_LOG_VOLUME", tbHttpTransportLogVolume); env.put("TB_MQTT_TRANSPORT_LOG_VOLUME", tbMqttTransportLogVolume); dockerCompose.withEnv(env); @@ -83,6 +87,9 @@ public class ThingsBoardDbInstaller extends ExternalResource { dockerCompose.withCommand("volume create " + tbCoapTransportLogVolume); dockerCompose.invokeDocker(); + dockerCompose.withCommand("volume create " + tbLwm2mTransportLogVolume); + dockerCompose.invokeDocker(); + dockerCompose.withCommand("volume create " + tbHttpTransportLogVolume); dockerCompose.invokeDocker(); @@ -107,11 +114,12 @@ public class ThingsBoardDbInstaller extends ExternalResource { protected void after() { copyLogs(tbLogVolume, "./target/tb-logs/"); copyLogs(tbCoapTransportLogVolume, "./target/tb-coap-transport-logs/"); + copyLogs(tbLwm2mTransportLogVolume, "./target/tb-lwm2m-transport-logs/"); copyLogs(tbHttpTransportLogVolume, "./target/tb-http-transport-logs/"); copyLogs(tbMqttTransportLogVolume, "./target/tb-mqtt-transport-logs/"); dockerCompose.withCommand("volume rm -f " + postgresDataVolume + " " + tbLogVolume + - " " + tbCoapTransportLogVolume + " " + tbHttpTransportLogVolume + " " + tbMqttTransportLogVolume); + " " + tbCoapTransportLogVolume + " " + tbLwm2mTransportLogVolume + " " + tbHttpTransportLogVolume + " " + tbMqttTransportLogVolume); dockerCompose.invokeDocker(); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java index 0d20661b25..855d2a926c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -44,7 +44,7 @@ public class HttpClientTest extends AbstractContainerTest { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("http_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); ResponseEntity deviceTelemetryResponse = restClient.getRestTemplate() @@ -73,7 +73,7 @@ public class HttpClientTest extends AbstractContainerTest { TB_TOKEN = restClient.getToken(); Device device = createDevice("test"); - String accessToken = restClient.getCredentials(device.getId()).getCredentialsId(); + String accessToken = restClient.getDeviceCredentialsByDeviceId(device.getId()).get().getCredentialsId(); assertNotNull(accessToken); ResponseEntity deviceSharedAttributes = restClient.getRestTemplate() @@ -92,6 +92,7 @@ public class HttpClientTest extends AbstractContainerTest { TimeUnit.SECONDS.sleep(3); + @SuppressWarnings("deprecation") Optional allOptional = restClient.getAttributes(accessToken, null, null); assertTrue(allOptional.isPresent()); @@ -101,6 +102,7 @@ public class HttpClientTest extends AbstractContainerTest { assertEquals(mapper.readTree(createPayload().toString()), all.get("shared")); assertEquals(mapper.readTree(createPayload().toString()), all.get("client")); + @SuppressWarnings("deprecation") Optional sharedOptional = restClient.getAttributes(accessToken, null, "stringKey"); assertTrue(sharedOptional.isPresent()); @@ -108,6 +110,7 @@ public class HttpClientTest extends AbstractContainerTest { assertEquals(shared.get("shared").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString())); assertFalse(shared.has("client")); + @SuppressWarnings("deprecation") Optional clientOptional = restClient.getAttributes(accessToken, "longKey,stringKey", null); assertTrue(clientOptional.isPresent()); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index f3212a53cc..c8156b6ef1 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -62,7 +62,7 @@ public class MqttClientTest extends AbstractContainerTest { public void telemetryUpload() throws Exception { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); MqttClient mqttClient = getMqttClient(deviceCredentials, null); @@ -89,7 +89,7 @@ public class MqttClientTest extends AbstractContainerTest { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS); MqttClient mqttClient = getMqttClient(deviceCredentials, null); @@ -113,7 +113,7 @@ public class MqttClientTest extends AbstractContainerTest { public void publishAttributeUpdateToServer() throws Exception { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); MqttMessageListener listener = new MqttMessageListener(); @@ -144,7 +144,7 @@ public class MqttClientTest extends AbstractContainerTest { public void requestAttributeValuesFromServer() throws Exception { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); MqttMessageListener listener = new MqttMessageListener(); @@ -204,7 +204,7 @@ public class MqttClientTest extends AbstractContainerTest { public void subscribeToAttributeUpdatesFromServer() throws Exception { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); MqttMessageListener listener = new MqttMessageListener(); MqttClient mqttClient = getMqttClient(deviceCredentials, listener); @@ -250,7 +250,7 @@ public class MqttClientTest extends AbstractContainerTest { public void serverSideRpc() throws Exception { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); MqttMessageListener listener = new MqttMessageListener(); MqttClient mqttClient = getMqttClient(deviceCredentials, listener); @@ -297,7 +297,7 @@ public class MqttClientTest extends AbstractContainerTest { public void clientSideRpc() throws Exception { restClient.login("tenant@thingsboard.org", "tenant"); Device device = createDevice("mqtt_"); - DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId()); + DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get(); MqttMessageListener listener = new MqttMessageListener(); MqttClient mqttClient = getMqttClient(deviceCredentials, listener); diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 8e7ba6d96e..75f009880b 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.2.1", + "version": "3.3.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.js", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 3e90099a76..a254b1acde 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/pom.xml b/msa/pom.xml index 686526d7b0..3d7bf3f347 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard msa @@ -50,7 +50,7 @@ com.spotify dockerfile-maven-plugin - 1.4.5 + 1.4.13 diff --git a/msa/tb-node/docker/Dockerfile b/msa/tb-node/docker/Dockerfile index 523ba440f0..21babfdfbf 100644 --- a/msa/tb-node/docker/Dockerfile +++ b/msa/tb-node/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk8 +FROM thingsboard/openjdk11 COPY start-tb-node.sh ${pkg.name}.deb /tmp/ diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index c9c0a2d4a2..133b81c79d 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/README.md b/msa/tb/README.md index 488ead2d85..6987c42bfa 100644 --- a/msa/tb/README.md +++ b/msa/tb/README.md @@ -21,7 +21,7 @@ In this example `thingsboard/tb` image will be used. You can choose any other im Execute the following command to run this docker directly: ` -$ docker run -it -p 9090:9090 -p 1883:1883 -p 5683:5683/udp -v ~/.mytb-data:/data --name mytb thingsboard/tb +$ docker run -it -p 9090:9090 -p 1883:1883 -p 5683:5683/udp -p 5685:5685/udp -v ~/.mytb-data:/data --name mytb thingsboard/tb ` Where: @@ -31,6 +31,7 @@ Where: - `-p 9090:9090` - connect local port 9090 to exposed internal HTTP port 9090 - `-p 1883:1883` - connect local port 1883 to exposed internal MQTT port 1883 - `-p 5683:5683` - connect local port 5683 to exposed internal COAP port 5683 +- `-p 5685:5685` - connect local port 5685 to exposed internal COAP port 5685 (lwm2m) - `-v ~/.mytb-data:/data` - mounts the host's dir `~/.mytb-data` to ThingsBoard DataBase data directory - `--name mytb` - friendly local name of this machine - `thingsboard/tb` - docker image, can be also `thingsboard/tb-postgres` or `thingsboard/tb-cassandra` @@ -45,6 +46,7 @@ Where: > $ VBoxManage controlvm "default" natpf1 "tcp-port9090,tcp,,9090,,9090" > $ VBoxManage controlvm "default" natpf1 "tcp-port1883,tcp,,1883,,1883" > $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5683,,5683" +> $ VBoxManage controlvm "default" natpf1 "tcp-port5683,tcp,,5685,,5685" > ``` After executing `docker run` command you can open `http://{your-host-ip}:9090` in you browser (for ex. `http://localhost:9090`). You should see ThingsBoard login page. diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index ef4b9ef3a4..f9eddd420c 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -14,14 +14,15 @@ # limitations under the License. # -FROM thingsboard/openjdk8 +FROM thingsboard/openjdk11 RUN apt-get update RUN apt-get install -y curl nmap procps -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null +RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -RUN echo 'deb http://www.apache.org/dist/cassandra/debian 311x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null -RUN curl -L https://www.apache.org/dist/cassandra/KEYS | apt-key add - +RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null +RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add - ENV PG_MAJOR=11 RUN apt-get update RUN apt-get install -y cassandra cassandra-tools postgresql-11 @@ -93,6 +94,7 @@ USER ${pkg.user} EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp +EXPOSE 5685/udp VOLUME ["/data"] diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index 4fe9116466..9b314a5a5d 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -14,11 +14,12 @@ # limitations under the License. # -FROM thingsboard/openjdk8 +FROM thingsboard/openjdk11 RUN apt-get update RUN apt-get install -y curl -RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null +RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null +RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - ENV PG_MAJOR 11 RUN apt-get update @@ -74,6 +75,7 @@ USER ${pkg.user} EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp +EXPOSE 5685/udp VOLUME ["/data"] diff --git a/msa/tb/docker-tb/Dockerfile b/msa/tb/docker-tb/Dockerfile index 0b6b248ae5..9920e6257c 100644 --- a/msa/tb/docker-tb/Dockerfile +++ b/msa/tb/docker-tb/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk8 +FROM thingsboard/openjdk11 COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ @@ -54,6 +54,7 @@ USER ${pkg.user} EXPOSE 9090 EXPOSE 1883 EXPOSE 5683/udp +EXPOSE 5685/udp VOLUME ["/data"] diff --git a/msa/tb/docker/thingsboard.conf b/msa/tb/docker/thingsboard.conf index 0acc4662c3..e583d90715 100644 --- a/msa/tb/docker/thingsboard.conf +++ b/msa/tb/docker/thingsboard.conf @@ -15,10 +15,10 @@ # export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data" -export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/thingsboard/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExitOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/thingsboard/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=thingsboard.out export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 5803f70bce..efb9a709e9 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/docker/Dockerfile b/msa/transport/coap/docker/Dockerfile index 6f377a0d91..813c6d76f2 100644 --- a/msa/transport/coap/docker/Dockerfile +++ b/msa/transport/coap/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk8 +FROM thingsboard/openjdk11 COPY start-tb-coap-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 056c38605c..1285cca0ce 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/docker/Dockerfile b/msa/transport/http/docker/Dockerfile index d29259384b..62cbe1d5f9 100644 --- a/msa/transport/http/docker/Dockerfile +++ b/msa/transport/http/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk8 +FROM thingsboard/openjdk11 COPY start-tb-http-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 3ba39a92e2..da0688b545 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/docker/Dockerfile b/msa/transport/lwm2m/docker/Dockerfile new file mode 100644 index 0000000000..9b4c2d60c2 --- /dev/null +++ b/msa/transport/lwm2m/docker/Dockerfile @@ -0,0 +1,33 @@ +# +# Copyright © 2016-2021 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. +# + +FROM thingsboard/openjdk11 + +COPY start-tb-lwm2m-transport.sh ${pkg.name}.deb /tmp/ + +RUN chmod a+x /tmp/*.sh \ + && mv /tmp/start-tb-lwm2m-transport.sh /usr/bin + +RUN yes | dpkg -i /tmp/${pkg.name}.deb +RUN rm /tmp/${pkg.name}.deb + +RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || : + +RUN chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar + +USER ${pkg.user} + +CMD ["start-tb-lwm2m-transport.sh"] diff --git a/msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh b/msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh new file mode 100755 index 0000000000..fc6a70c0c5 --- /dev/null +++ b/msa/transport/lwm2m/docker/start-tb-lwm2m-transport.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Copyright © 2016-2021 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. +# + +CONF_FOLDER="/config" +jarfile=${pkg.installFolder}/bin/${pkg.name}.jar +configfile=${pkg.name}.conf + +source "${CONF_FOLDER}/${configfile}" + +export LOADER_PATH=/config,${LOADER_PATH} + +echo "Starting '${project.name}' ..." + +cd ${pkg.installFolder}/bin + +exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.lwm2m.ThingsboardLwm2mTransportApplication \ + -Dspring.jpa.hibernate.ddl-auto=none \ + -Dlogging.config=/config/logback.xml \ + org.springframework.boot.loader.PropertiesLauncher diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml new file mode 100644 index 0000000000..7d8d05d5e6 --- /dev/null +++ b/msa/transport/lwm2m/pom.xml @@ -0,0 +1,190 @@ + + + 4.0.0 + + org.thingsboard.msa + 3.3.0-SNAPSHOT + transport + + org.thingsboard.msa.transport + lwm2m + pom + + ThingsBoard LWM2M Transport Microservice + https://thingsboard.io + ThingsBoard LWM2M Transport Microservice + + + UTF-8 + ${basedir}/../../.. + tb-lwm2m-transport + tb-lwm2m-transport + /var/log/${pkg.name} + /usr/share/${pkg.name} + + + + + org.thingsboard.transport + lwm2m + ${project.version} + deb + deb + provided + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-tb-lwm2m-transport-deb + package + + copy + + + + + org.thingsboard.transport + lwm2m + deb + deb + ${pkg.name}.deb + ${project.build.directory} + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-docker-config + process-resources + + copy-resources + + + ${project.build.directory} + + + docker + true + + + + + + + + com.spotify + dockerfile-maven-plugin + + + build-docker-image + pre-integration-test + + build + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + true + false + ${project.build.directory} + + + + tag-docker-image + pre-integration-test + + tag + + + ${dockerfile.skip} + ${docker.repo}/${docker.name} + ${project.version} + + + + + + + + + push-docker-image + + + push-docker-image + + + + + + com.spotify + dockerfile-maven-plugin + + + push-latest-docker-image + pre-integration-test + + push + + + latest + ${docker.repo}/${docker.name} + + + + push-version-docker-image + pre-integration-test + + push + + + ${project.version} + ${docker.repo}/${docker.name} + + + + + + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + + diff --git a/msa/transport/mqtt/docker/Dockerfile b/msa/transport/mqtt/docker/Dockerfile index 1052f89f78..ee41f227db 100644 --- a/msa/transport/mqtt/docker/Dockerfile +++ b/msa/transport/mqtt/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk8 +FROM thingsboard/openjdk11 COPY start-tb-mqtt-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 279a03c03d..782b6a228a 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 6a9fe17fc2..02defc562c 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT msa org.thingsboard.msa @@ -38,19 +38,7 @@ mqtt http coap + lwm2m snmp - - - - - - com.spotify - dockerfile-maven-plugin - 1.4.5 - - - - - diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index df5caa658c..62448d8763 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 792926a86d..3897c31f5e 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "3.2.1", + "version": "3.3.0", "description": "ThingsBoard Web UI Microservice", "main": "server.js", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index ffd1ba2e16..85f5db6ce3 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index f778d83c44..742aa039be 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard netty-mqtt - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT jar Netty MQTT Client @@ -67,16 +67,11 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 - - 1.8 - 1.8 - org.apache.maven.plugins maven-jar-plugin - 2.4 + 3.1.1 @@ -87,4 +82,4 @@ - \ No newline at end of file + diff --git a/packaging/java/build.gradle b/packaging/java/build.gradle index aed7f8a5ba..fcc937e53f 100644 --- a/packaging/java/build.gradle +++ b/packaging/java/build.gradle @@ -102,7 +102,7 @@ buildRpm { archiveVersion = projectVersion.replace('-', '') archiveFileName = "${pkgName}.rpm" - requires("java-1.8.0") + requires("java-11") from("${buildDir}/conf") { include "${pkgName}.conf" @@ -141,7 +141,7 @@ buildDeb { archiveFileName = "${pkgName}.deb" - requires("openjdk-8-jre").or("java8-runtime").or("oracle-java8-installer").or("openjdk-8-jre-headless") + requires("openjdk-11-jre").or("java11-runtime").or("oracle-java11-installer").or("openjdk-11-jre-headless") from("${buildDir}/conf") { include "${pkgName}.conf" diff --git a/packaging/java/scripts/windows/install.bat b/packaging/java/scripts/windows/install.bat index ffd085aa20..6f168c76d8 100644 --- a/packaging/java/scripts/windows/install.bat +++ b/packaging/java/scripts/windows/install.bat @@ -7,11 +7,11 @@ setlocal ENABLEEXTENSIONS for /f tokens^=2-5^ delims^=.-_^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j%%k" @ECHO CurrentVersion %jver% -if %jver% NEQ 18 GOTO JAVA_NOT_INSTALLED +if %jver% NEQ 110 GOTO JAVA_NOT_INSTALLED :JAVA_INSTALLED -@ECHO Java 1.8 found! +@ECHO Java 11 found! @ECHO Installing thingsboard ... SET loadDemo=false @@ -50,8 +50,8 @@ POPD GOTO END :JAVA_NOT_INSTALLED -@ECHO Java 1.8 is not installed. Only Java 1.8 is supported -@ECHO Please go to https://adoptopenjdk.net/index.html and install Java 1.8. Then retry installation. +@ECHO Java 11 is not installed. Only Java 11 is supported +@ECHO Please go to https://adoptopenjdk.net/index.html and install Java 11. Then retry installation. PAUSE GOTO END diff --git a/packaging/java/scripts/windows/service.xml b/packaging/java/scripts/windows/service.xml index 2cc46a7e91..7b077142c4 100644 --- a/packaging/java/scripts/windows/service.xml +++ b/packaging/java/scripts/windows/service.xml @@ -10,29 +10,18 @@ java -Dplatform=windows -Dinstall.data_dir=%BASE%\data - -Xloggc:%BASE%\logs\gc.log + -Xlog:gc*,heap*,age*,safepoint=debug:file=%BASE%\logs\gc.log:time,uptime,level,tags:filecount=10,filesize=10M -XX:+HeapDumpOnOutOfMemoryError - -XX:+PrintGCDetails - -XX:+PrintGCDateStamps - -XX:+PrintHeapAtGC - -XX:+PrintTenuringDistribution - -XX:+PrintGCApplicationStoppedTime - -XX:+UseGCLogFileRotation - -XX:NumberOfGCLogFiles=10 - -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark - -XX:CMSWaitDuration=10000 - -XX:+UseParNewGC - -XX:+UseConcMarkSweepGC - -XX:+CMSParallelRemarkEnabled - -XX:+CMSParallelInitialMarkEnabled - -XX:+CMSEdenChunksRecordAlways - -XX:CMSInitiatingOccupancyFraction=75 - -XX:+UseCMSInitiatingOccupancyOnly + -XX:+UseG1GC + -XX:MaxGCPauseMillis=500 + -XX:+UseStringDeduplication + -XX:+ParallelRefProcEnabled + -XX:MaxTenuringThreshold=10 -Xms512m -Xmx1024m -jar diff --git a/pom.xml b/pom.xml index 063cb027cb..38ac0c5963 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT pom Thingsboard @@ -36,6 +36,9 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} + 1.3.2 + 2.3.2 + 2.3.2 2.3.5.RELEASE 5.2.10.RELEASE 5.4.1 @@ -46,9 +49,9 @@ 4.12 1.7.7 1.2.3 - 1.9.5 + 3.3.3 0.10 - 4.6.0 + 4.10.0 4.0.5 4.3.1.0 3.11.9 @@ -58,11 +61,14 @@ 3.4 2.5 1.4 - 2.11.3 - 2.11.3 - 2.11.3 + 2.12.1 + 2.12.1 + 2.12.1 2.2.6 - 1.0.2 + 2.6.1 + 1.3.0 + 1.3.0 + 1.3.0 2.6.2 2.3.30 1.6.2 @@ -70,7 +76,7 @@ 3.5.5 3.11.4 1.22.1 - 1.16.18 + 1.18.18 1.2.4 4.1.53.Final 1.5.0 @@ -86,12 +92,12 @@ 2.5.0 2.5.3 1.2.1 - 9.4.1212 + 42.2.16 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 5.0.2 - 0.1.14 + 0.1.31 2.6.0 4.1.1 2.57 @@ -255,7 +261,8 @@
- ${main.dir}/packaging/${pkg.type}/filters/windows.properties + ${main.dir}/packaging/${pkg.type}/filters/windows.properties + @@ -316,7 +323,8 @@
- ${main.dir}/packaging/${pkg.type}/filters/windows.properties + ${main.dir}/packaging/${pkg.type}/filters/windows.properties + @@ -338,7 +346,8 @@
- ${main.dir}/packaging/${pkg.type}/filters/windows.properties + ${main.dir}/packaging/${pkg.type}/filters/windows.properties + @@ -362,9 +371,39 @@ false
+ + ../common/transport/lwm2m/src/main/resources + + **/*.xml + **/*.jks + + false + + + + + + copy-lwm2m-resources + ${pkg.process-resources.phase} + + copy-resources + + + ../transport/lwm2m/src/main/data + + + ../common/transport/lwm2m/src/main/resources + + **/*.xml + **/*.jks + + false + + + copy-docker-config ${pkg.process-resources.phase} @@ -544,10 +583,21 @@ org.apache.maven.plugins maven-compiler-plugin - 2.5.1 + 3.8.1 - 1.8 - 1.8 + 11 + + -Xlint:deprecation + -Xlint:removal + -Xlint:unchecked + + + + org.projectlombok + lombok + ${lombok.version} + + @@ -558,18 +608,23 @@ org.apache.maven.plugins maven-source-plugin - 2.2.1 + 3.2.1 org.apache.maven.plugins maven-jar-plugin - 3.0.2 + 3.1.1 org.apache.maven.plugins maven-assembly-plugin 3.0.0 + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + org.springframework.boot spring-boot-maven-plugin @@ -584,12 +639,22 @@ org.apache.maven.plugins maven-surefire-plugin 3.0.0-M1 + + + --illegal-access=permit + + org.apache.maven.plugins maven-install-plugin 3.0.0-M1 + + org.apache.maven.plugins + maven-deploy-plugin + 3.0.0-M1 + org.apache.maven.plugins maven-dependency-plugin @@ -729,6 +794,10 @@ **/*.proto.js docker/haproxy/** docker/tb-node/** + src/main/resources/models/*.xml + src/main/resources/credentials/*.jks + src/main/resources/credentials/shell/*.jks + src/main/resources/credentials/shell/*.jks.old ui/** src/.browserslistrc **/yarn.lock @@ -789,6 +858,11 @@ util ${project.version} + + org.thingsboard.common + cache + ${project.version} + org.thingsboard.common actor @@ -844,6 +918,11 @@ snmp ${project.version} + + org.thingsboard.common.transport + lwm2m + ${project.version} + org.thingsboard dao @@ -878,6 +957,21 @@ test-jar test + + javax.annotation + javax.annotation-api + ${javax-annotation.version} + + + jakarta.xml.bind + jakarta.xml.bind-api + ${jakarta.xml.bind-api.version} + + + org.glassfish.jaxb + jaxb-runtime + ${jaxb-runtime.version} + org.springframework.boot spring-boot-starter-security @@ -1108,10 +1202,44 @@ + + org.eclipse.leshan + leshan-server-cf + ${leshan-server.version} + + + org.eclipse.leshan + leshan-client-cf + ${leshan-client.version} + + + org.eclipse.leshan + leshan-server-redis + ${leshan-server.version} + + + org.eclipse.leshan + leshan-core + ${leshan-core.version} + org.eclipse.californium californium-core ${californium.version} + test-jar + test + + + org.eclipse.californium + californium-core + ${californium.version} + + + org.eclipse.californium + element-connector + ${californium.version} + test-jar + test com.google.code.gson @@ -1225,7 +1353,7 @@ org.mockito - mockito-all + mockito-core ${mockito.version} test diff --git a/rest-client/pom.xml b/rest-client/pom.xml index d170dec0e2..e45db432d8 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard rest-client diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 151edf9e8f..2528e5f36c 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -121,6 +121,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -1091,6 +1092,24 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody(); } + public Optional saveDeviceWithCredentials(Device device, DeviceCredentials credentials) { + try { + Map, Object> deviceCredentials = new ConcurrentHashMap<>(); + deviceCredentials.put(Device.class, device); + deviceCredentials.put(DeviceCredentials.class, credentials); +// return restTemplate.postForEntity(baseURL + "/api/lwm2m/device-credentials", deviceCredentials, Device.class).getBody(); + ResponseEntity deviceOpt = restTemplate.postForEntity(baseURL + "/api/lwm2m/device-credentials", deviceCredentials, Device.class); + return Optional.ofNullable(deviceOpt.getBody()); + } catch (HttpClientErrorException exception) { + if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { + return Optional.empty(); + } else { + throw exception; + } + } + } + + public PageData getTenantDevices(String type, PageLink pageLink) { Map params = new HashMap<>(); params.put("type", type); @@ -1170,7 +1189,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { public List getDeviceTypes() { return restTemplate.exchange( - baseURL + "/api/devices", + baseURL + "/api/device/types", HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2188,7 +2207,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/tenantProfiles" + getUrlParams(pageLink), + baseURL + "/api/tenantProfiles?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2199,7 +2218,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/tenantProfileInfos" + getUrlParams(pageLink), + baseURL + "/api/tenantProfileInfos?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -2256,7 +2275,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { Map params = new HashMap<>(); addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/users" + getUrlParams(pageLink), + baseURL + "/api/users?" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 2e968bfb46..d4af78f871 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 613493b0d5..bb348793c1 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine @@ -88,6 +88,17 @@ javax.mail provided + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + test + diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index d7188f4456..5f182c2c34 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId; 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.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -161,6 +162,10 @@ public interface TbContext { RuleNodeId getSelfId(); + RuleNode getSelf(); + + String getRuleChainName(); + TenantId getTenantId(); AttributesService getAttributesService(); @@ -225,9 +230,6 @@ public interface TbContext { TbResultSetFuture submitCassandraTask(CassandraStatementTask task); - @Deprecated - RedisTemplate getRedisTemplate(); - PageData findRuleNodeStates(PageLink pageLink); RuleNodeState findRuleNodeStateForEntity(EntityId entityId); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceCredentialsUpdateNotificationMsg.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceCredentialsUpdateNotificationMsg.java index ff1f1e40b1..4b039dbf53 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceCredentialsUpdateNotificationMsg.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceCredentialsUpdateNotificationMsg.java @@ -21,6 +21,8 @@ import lombok.ToString; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKey; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.MsgType; import java.util.Set; @@ -31,9 +33,17 @@ import java.util.Set; @Data public class DeviceCredentialsUpdateNotificationMsg implements ToDeviceActorNotificationMsg { + private static final long serialVersionUID = -3956907402411126990L; + private final TenantId tenantId; private final DeviceId deviceId; + /** + * LwM2M + * @return + */ + private final DeviceCredentials deviceCredentials; + @Override public MsgType getMsgType() { return MsgType.DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceNameOrTypeUpdateMsg.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceNameOrTypeUpdateMsg.java index e92f0f2842..0418986998 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceNameOrTypeUpdateMsg.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/msg/DeviceNameOrTypeUpdateMsg.java @@ -24,6 +24,9 @@ import org.thingsboard.server.common.msg.MsgType; @Data @AllArgsConstructor public class DeviceNameOrTypeUpdateMsg implements ToDeviceActorNotificationMsg { + + private static final long serialVersionUID = -5738949227650536685L; + private final TenantId tenantId; private final DeviceId deviceId; private final String deviceName; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index 8804320295..f44c2a48d6 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -16,15 +16,20 @@ package org.thingsboard.rule.engine.api.util; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -34,8 +39,11 @@ public class TbNodeUtils { private static final ObjectMapper mapper = new ObjectMapper(); - private static final String VARIABLE_TEMPLATE = "${%s}"; + private static final String METADATA_VARIABLE_TEMPLATE = "${%s}"; + private static final Pattern DATA_PATTERN = Pattern.compile("(\\$\\[)(.*?)(])"); + + private static final String DATA_VARIABLE_TEMPLATE = "$[%s]"; public static T convert(TbNodeConfiguration configuration, Class clazz) throws TbNodeException { try { @@ -45,6 +53,43 @@ public class TbNodeUtils { } } + public static List processPatterns(List patterns, TbMsg tbMsg) { + if (!CollectionUtils.isEmpty(patterns)) { + return patterns.stream().map(p -> processPattern(p, tbMsg)).collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + public static String processPattern(String pattern, TbMsg tbMsg) { + try { + String result = processPattern(pattern, tbMsg.getMetaData()); + JsonNode json = mapper.readTree(tbMsg.getData()); + if (json.isObject()) { + Matcher matcher = DATA_PATTERN.matcher(result); + while (matcher.find()) { + String group = matcher.group(2); + String[] keys = group.split("\\."); + JsonNode jsonNode = json; + for (String key : keys) { + if (!StringUtils.isEmpty(key) && jsonNode != null) { + jsonNode = jsonNode.get(key); + } else { + jsonNode = null; + break; + } + } + + if (jsonNode != null && jsonNode.isValueNode()) { + result = result.replace(String.format(DATA_VARIABLE_TEMPLATE, group), jsonNode.asText()); + } + } + } + return result; + } catch (Exception e) { + throw new RuntimeException("Failed to process pattern!", e); + } + } + public static List processPatterns(List patterns, TbMsgMetaData metaData) { if (!CollectionUtils.isEmpty(patterns)) { return patterns.stream().map(p -> processPattern(p, metaData)).collect(Collectors.toList()); @@ -53,15 +98,15 @@ public class TbNodeUtils { } public static String processPattern(String pattern, TbMsgMetaData metaData) { - String result = new String(pattern); - for (Map.Entry keyVal : metaData.values().entrySet()) { + String result = pattern; + for (Map.Entry keyVal : metaData.values().entrySet()) { result = processVar(result, keyVal.getKey(), keyVal.getValue()); } return result; } private static String processVar(String pattern, String key, String val) { - String varPattern = String.format(VARIABLE_TEMPLATE, key); + String varPattern = String.format(METADATA_VARIABLE_TEMPLATE, key); return pattern.replace(varPattern, val); } diff --git a/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java new file mode 100644 index 0000000000..11687e0415 --- /dev/null +++ b/rule-engine/rule-engine-api/src/test/java/org/thingsboard/rule/engine/api/util/TbNodeUtilsTest.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.api.util; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.common.util.JacksonUtil; + +@RunWith(MockitoJUnitRunner.class) +public class TbNodeUtilsTest { + + @Test + public void testSimpleReplacement() { + String pattern = "ABC ${metadata_key} $[data_key]"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("metadata_key", "metadata_value"); + + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("data_key", "data_value"); + + TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + String result = TbNodeUtils.processPattern(pattern, msg); + Assert.assertEquals("ABC metadata_value data_value", result); + } + + @Test + public void testNoReplacement() { + String pattern = "ABC ${metadata_key} $[data_key]"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("key", "metadata_value"); + + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("key", "data_value"); + + TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + String result = TbNodeUtils.processPattern(pattern, msg); + Assert.assertEquals(pattern, result); + } + + @Test + public void testSameKeysReplacement() { + String pattern = "ABC ${key} $[key]"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("key", "metadata_value"); + + ObjectNode node = JacksonUtil.newObjectNode(); + node.put("key", "data_value"); + + TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + String result = TbNodeUtils.processPattern(pattern, msg); + Assert.assertEquals("ABC metadata_value data_value", result); + } + + @Test + public void testComplexObjectReplacement() { + String pattern = "ABC ${key} $[key1.key2.key3]"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("key", "metadata_value"); + + ObjectNode key2Node = JacksonUtil.newObjectNode(); + key2Node.put("key3", "value3"); + + ObjectNode key1Node = JacksonUtil.newObjectNode(); + key1Node.set("key2", key2Node); + + + ObjectNode node = JacksonUtil.newObjectNode(); + node.set("key1", key1Node); + + TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + String result = TbNodeUtils.processPattern(pattern, msg); + Assert.assertEquals("ABC metadata_value value3", result); + } + + @Test + public void testArrayReplacementDoesNotWork() { + String pattern = "ABC ${key} $[key1.key2[0].key3]"; + TbMsgMetaData md = new TbMsgMetaData(); + md.putValue("key", "metadata_value"); + + ObjectNode key2Node = JacksonUtil.newObjectNode(); + key2Node.put("key3", "value3"); + + ObjectNode key1Node = JacksonUtil.newObjectNode(); + key1Node.set("key2", key2Node); + + + ObjectNode node = JacksonUtil.newObjectNode(); + node.set("key1", key1Node); + + TbMsg msg = TbMsg.newMsg("CUSTOM", TenantId.SYS_TENANT_ID, md, JacksonUtil.toString(node)); + String result = TbNodeUtils.processPattern(pattern, msg); + Assert.assertEquals("ABC metadata_value $[key1.key2[0].key3]", result); + } + +} diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 48863dac3e..31c0c1d824 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine @@ -148,7 +148,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java index 73ca51f1e9..b2ec1cf7af 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java @@ -28,7 +28,7 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import static org.thingsboard.common.util.DonAsynchron.withCallback; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java index 482e5261f0..39f8b6fa3a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java @@ -48,7 +48,7 @@ public abstract class TbAbstractCustomerActionNode cacheBuilder = CacheBuilder.newBuilder(); if (this.config.getCustomerCacheExpiration() > 0) { cacheBuilder.expireAfterWrite(this.config.getCustomerCacheExpiration(), TimeUnit.SECONDS); } @@ -79,7 +79,7 @@ public abstract class TbAbstractCustomerActionNode getCustomer(TbContext ctx, TbMsg msg) { - String customerTitle = TbNodeUtils.processPattern(this.config.getCustomerNamePattern(), msg.getMetaData()); + String customerTitle = TbNodeUtils.processPattern(this.config.getCustomerNamePattern(), msg); CustomerKey key = new CustomerKey(customerTitle); return ctx.getDbCallbackExecutor().executeAsync(() -> { Optional customerId = customerIdCache.get(key); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java index 66a8f28e87..12bbdf450d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java @@ -70,7 +70,7 @@ public abstract class TbAbstractRelationActionNode cacheBuilder = CacheBuilder.newBuilder(); if (this.config.getEntityCacheExpiration() > 0) { cacheBuilder.expireAfterWrite(this.config.getEntityCacheExpiration(), TimeUnit.SECONDS); } @@ -140,7 +140,7 @@ public abstract class TbAbstractRelationActionNode processAlarm(TbContext ctx, TbMsg msg) { - String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg.getMetaData()); + String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg); ListenableFuture alarmFuture; if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { alarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index db95a5986c..152f8fbe49 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -76,7 +76,7 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { if (!msg.getMetaData().getData().isEmpty()) { long now = System.currentTimeMillis(); String scope = msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name()) ? - DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue("scope"); + DataConstants.CLIENT_SCOPE : msg.getMetaData().getValue(DataConstants.SCOPE); ListenableFuture> entityViewsFuture = ctx.getEntityViewService().findEntityViewsByTenantIdAndEntityIdAsync(ctx.getTenantId(), msg.getOriginator()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index dcae930fbd..c941d6ca92 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -70,7 +70,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode messageAttributes = new HashMap<>(); this.config.getMessageAttributes().forEach((k,v) -> { - String name = TbNodeUtils.processPattern(k, msg.getMetaData()); - String val = TbNodeUtils.processPattern(v, msg.getMetaData()); + String name = TbNodeUtils.processPattern(k, msg); + String val = TbNodeUtils.processPattern(v, msg); messageAttributes.put(name, new MessageAttributeValue().withDataType("String").withStringValue(val)); }); sendMsgRequest.setMessageAttributes(messageAttributes); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/AnonymousCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/AnonymousCredentials.java new file mode 100644 index 0000000000..6670254fd1 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/AnonymousCredentials.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2021 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.credentials; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AnonymousCredentials implements ClientCredentials { + @Override + public CredentialsType getType() { + return CredentialsType.ANONYMOUS; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/BasicCredentials.java similarity index 61% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/BasicCredentials.java index 46fe584a63..c958d9300f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/AnonymousCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/BasicCredentials.java @@ -13,23 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.mqtt.credentials; +package org.thingsboard.rule.engine.credentials; -import io.netty.handler.ssl.SslContext; -import org.thingsboard.mqtt.MqttClientConfig; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; -import java.util.Optional; - -public class AnonymousCredentials implements MqttClientCredentials { - - @Override - public Optional initSslContext() { - return Optional.empty(); - } +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class BasicCredentials implements ClientCredentials { + private String username; + private String password; @Override - public void configure(MqttClientConfig config) { - + public CredentialsType getType() { + return CredentialsType.BASIC; } } - diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java similarity index 90% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java index 18c0b132f6..54be64b7fa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/CertPemClientCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.mqtt.credentials; +package org.thingsboard.rule.engine.credentials; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import lombok.Data; @@ -29,7 +28,6 @@ import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.springframework.util.StringUtils; -import org.thingsboard.mqtt.MqttClientConfig; import javax.crypto.Cipher; import javax.crypto.EncryptedPrivateKeyInfo; @@ -51,40 +49,41 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Optional; @Data @Slf4j @JsonIgnoreProperties(ignoreUnknown = true) -public class CertPemClientCredentials implements MqttClientCredentials { - +public class CertPemCredentials implements ClientCredentials { private static final String TLS_VERSION = "TLSv1.2"; - private String caCert; + protected String caCert; private String cert; private String privateKey; private String password; @Override - public Optional initSslContext() { + public CredentialsType getType() { + return CredentialsType.CERT_PEM; + } + + @Override + public SslContext initSslContext() { try { Security.addProvider(new BouncyCastleProvider()); - return Optional.of(SslContextBuilder.forClient() - .keyManager(createAndInitKeyManagerFactory()) - .trustManager(createAndInitTrustManagerFactory()) - .clientAuth(ClientAuth.REQUIRE) - .build()); + SslContextBuilder builder = SslContextBuilder.forClient(); + if (StringUtils.hasLength(caCert)) { + builder.trustManager(createAndInitTrustManagerFactory()); + } + if (StringUtils.hasLength(cert) && StringUtils.hasLength(privateKey)) { + builder.keyManager(createAndInitKeyManagerFactory()); + } + return builder.build(); } catch (Exception e) { log.error("[{}:{}] Creating TLS factory failed!", caCert, cert, e); throw new RuntimeException("Creating TLS factory failed!", e); } } - @Override - public void configure(MqttClientConfig config) { - - } - private KeyManagerFactory createAndInitKeyManagerFactory() throws Exception { X509Certificate certHolder = readCertFile(cert); Object keyObject = readPrivateKeyFile(privateKey); @@ -122,7 +121,7 @@ public class CertPemClientCredentials implements MqttClientCredentials { return keyManagerFactory; } - private TrustManagerFactory createAndInitTrustManagerFactory() throws Exception { + protected TrustManagerFactory createAndInitTrustManagerFactory() throws Exception { X509Certificate caCertHolder; caCertHolder = readCertFile(caCert); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/ClientCredentials.java similarity index 66% rename from rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java rename to rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/ClientCredentials.java index 3b6b3b9026..8f4b20a5c4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/credentials/MqttClientCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/ClientCredentials.java @@ -13,29 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.rule.engine.mqtt.credentials; +package org.thingsboard.rule.engine.credentials; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.netty.handler.ssl.SslContext; -import org.thingsboard.mqtt.MqttClientConfig; +import io.netty.handler.ssl.SslContextBuilder; import org.thingsboard.rule.engine.mqtt.azure.AzureIotHubSasCredentials; -import java.util.Optional; +import javax.net.ssl.SSLException; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = AnonymousCredentials.class, name = "anonymous"), @JsonSubTypes.Type(value = BasicCredentials.class, name = "basic"), @JsonSubTypes.Type(value = AzureIotHubSasCredentials.class, name = "sas"), - @JsonSubTypes.Type(value = CertPemClientCredentials.class, name = "cert.PEM")}) -public interface MqttClientCredentials { + @JsonSubTypes.Type(value = CertPemCredentials.class, name = "cert.PEM")}) +public interface ClientCredentials { + @JsonIgnore + CredentialsType getType(); - Optional initSslContext(); - - void configure(MqttClientConfig config); + @JsonIgnore + default SslContext initSslContext() throws SSLException{ + return SslContextBuilder.forClient().build(); + } } - diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CredentialsType.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CredentialsType.java new file mode 100644 index 0000000000..a0d298a9be --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CredentialsType.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 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.credentials; + +public enum CredentialsType { + ANONYMOUS("anonymous"), + BASIC("basic"), + SAS("sas"), + CERT_PEM("cert.PEM"); + + private final String label; + + CredentialsType(String label) { + this.label = label; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java index 295690d445..46e3c97e87 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.data; import lombok.Data; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.List; @@ -26,6 +26,6 @@ public class RelationsQuery { private EntitySearchDirection direction; private int maxLevel = 1; - private List filters; + private List filters; private boolean fetchLastLevelOnly = false; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 69482a7b89..6047370193 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -104,9 +104,10 @@ public class TbMsgGeneratorNode implements TbNode { } }, t -> { - if (initialized) { + if (initialized && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { ctx.tellFailure(msg, t); scheduleTickMsg(ctx); + currentMsgCount++; } }); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index d72c56981e..9192f0a55f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -84,7 +84,7 @@ public class TbMsgDelayNode implements TbNode { int periodInSeconds; if (config.isUseMetadataPeriodInSecondsPatterns()) { if (isParsable(msg, config.getPeriodInSecondsPattern())) { - periodInSeconds = Integer.parseInt(TbNodeUtils.processPattern(config.getPeriodInSecondsPattern(), msg.getMetaData())); + periodInSeconds = Integer.parseInt(TbNodeUtils.processPattern(config.getPeriodInSecondsPattern(), msg)); } else { throw new RuntimeException("Can't parse period in seconds from metadata using pattern: " + config.getPeriodInSecondsPattern()); } @@ -95,7 +95,7 @@ public class TbMsgDelayNode implements TbNode { } private boolean isParsable(TbMsg msg, String pattern) { - return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg.getMetaData())); + return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg)); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index c04de9e070..17c49adfc7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -91,8 +91,8 @@ public class TbPubSubNode implements TbNode { PubsubMessage.Builder pubsubMessageBuilder = PubsubMessage.newBuilder(); pubsubMessageBuilder.setData(data); this.config.getMessageAttributes().forEach((k, v) -> { - String name = TbNodeUtils.processPattern(k, msg.getMetaData()); - String val = TbNodeUtils.processPattern(v, msg.getMetaData()); + String name = TbNodeUtils.processPattern(k, msg); + String val = TbNodeUtils.processPattern(v, msg); pubsubMessageBuilder.putAttributes(name, val); }); ApiFuture messageIdFuture = this.pubSubClient.publish(pubsubMessageBuilder.build()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java index 6ce7de8baf..b5612dfc6a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java @@ -62,7 +62,7 @@ public class GeoUtil { } polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(firstLng), jtsCtx.getShapeFactory().normY(firstLat)); Shape shape = polygonBuilder.buildOrRect(); - Point point = jtsCtx.makePoint(coordinates.getLongitude(), coordinates.getLatitude()); + Point point = jtsCtx.getShapeFactory().pointXY(coordinates.getLongitude(), coordinates.getLatitude()); return shape.relate(point).equals(SpatialRelation.CONTAINS); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index 89cc747811..0ac03e1afb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -17,11 +17,13 @@ package org.thingsboard.rule.engine.kafka; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; +import org.apache.kafka.common.serialization.StringSerializer; import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.header.internals.RecordHeader; import org.apache.kafka.common.header.internals.RecordHeaders; @@ -73,8 +75,8 @@ public class TbKafkaNode implements TbNode { Properties properties = new Properties(); properties.put(ProducerConfig.CLIENT_ID_CONFIG, "producer-tb-kafka-node-" + ctx.getSelfId().getId().toString() + "-" + ctx.getServiceId()); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); - properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer()); - properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer()); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, getKafkaSerializerClass(config.getValueSerializer())); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, getKafkaSerializerClass(config.getKeySerializer())); properties.put(ProducerConfig.ACKS_CONFIG, config.getAcks()); properties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries()); properties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize()); @@ -92,9 +94,22 @@ public class TbKafkaNode implements TbNode { } } + private Class getKafkaSerializerClass(String serializerClassName) { + Class serializerClass = null; + if (!StringUtils.isEmpty(serializerClassName)) { + try { + serializerClass = Class.forName(serializerClassName); + } catch (ClassNotFoundException e) {} + } + if (serializerClass == null) { + serializerClass = StringSerializer.class; + } + return serializerClass; + } + @Override public void onMsg(TbContext ctx, TbMsg msg) { - String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg.getMetaData()); + String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg); try { ctx.getExternalCallExecutor().executeAsync(() -> { publish(ctx, msg, topic); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index 854c89e720..8372ebdd10 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -72,18 +72,18 @@ public class TbMsgToEmailNode implements TbNode { private EmailPojo convert(TbMsg msg) throws IOException { EmailPojo.EmailPojoBuilder builder = EmailPojo.builder(); - builder.from(fromTemplate(this.config.getFromTemplate(), msg.getMetaData())); - builder.to(fromTemplate(this.config.getToTemplate(), msg.getMetaData())); - builder.cc(fromTemplate(this.config.getCcTemplate(), msg.getMetaData())); - builder.bcc(fromTemplate(this.config.getBccTemplate(), msg.getMetaData())); - builder.subject(fromTemplate(this.config.getSubjectTemplate(), msg.getMetaData())); - builder.body(fromTemplate(this.config.getBodyTemplate(), msg.getMetaData())); + builder.from(fromTemplate(this.config.getFromTemplate(), msg)); + builder.to(fromTemplate(this.config.getToTemplate(), msg)); + builder.cc(fromTemplate(this.config.getCcTemplate(), msg)); + builder.bcc(fromTemplate(this.config.getBccTemplate(), msg)); + builder.subject(fromTemplate(this.config.getSubjectTemplate(), msg)); + builder.body(fromTemplate(this.config.getBodyTemplate(), msg)); return builder.build(); } - private String fromTemplate(String template, TbMsgMetaData metaData) { + private String fromTemplate(String template, TbMsg msg) { if (!StringUtils.isEmpty(template)) { - return TbNodeUtils.processPattern(template, metaData); + return TbNodeUtils.processPattern(template, msg); } else { return null; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java new file mode 100644 index 0000000000..31f3aa969e --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -0,0 +1,185 @@ +/** + * Copyright © 2016-2021 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.metadata; + +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.extern.slf4j.Slf4j; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.common.util.JacksonUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@RuleNode(type = ComponentType.ENRICHMENT, + name = "calculate delta", relationTypes = {"Success", "Failure", "Other"}, + configClazz = CalculateDeltaNodeConfiguration.class, + nodeDescription = "Calculates and adds 'delta' value into message based on the incoming and previous value", + nodeDetails = "Calculates delta and period based on the previous time-series reading and current data. " + + "Delta calculation is done in scope of the message originator, e.g. device, asset or customer. " + + "If there is input key, the output relation will be 'Success' unless delta is negative and corresponding configuration parameter is set. " + + "If there is no input value key in the incoming message, the output relation will be 'Other'.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbEnrichmentNodeCalculateDeltaConfig") +public class CalculateDeltaNode implements TbNode { + private Map cache; + private CalculateDeltaNodeConfiguration config; + private TbContext ctx; + private TimeseriesService timeseriesService; + private boolean useCache; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, CalculateDeltaNodeConfiguration.class); + this.ctx = ctx; + this.timeseriesService = ctx.getTimeseriesService(); + this.useCache = config.isUseCache(); + + if (useCache) { + cache = new ConcurrentHashMap<>(); + } + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) { + JsonNode json = JacksonUtil.toJsonNode(msg.getData()); + String inputKey = config.getInputValueKey(); + if (json.has(inputKey)) { + DonAsynchron.withCallback(getLastValue(msg.getOriginator()), + previousData -> { + double currentValue = json.get(inputKey).asDouble(); + long currentTs = TbMsgTimeseriesNode.getTs(msg); + + if (useCache) { + cache.put(msg.getOriginator(), new ValueWithTs(currentTs, currentValue)); + } + + BigDecimal delta = BigDecimal.valueOf(previousData != null ? currentValue - previousData.value : 0.0); + + if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) { + ctx.tellNext(msg, TbRelationTypes.FAILURE); + return; + } + + + if (config.getRound() != null) { + delta = delta.setScale(config.getRound(), RoundingMode.HALF_UP); + } + + ObjectNode result = (ObjectNode) json; + if (delta.stripTrailingZeros().scale() > 0) { + result.put(config.getOutputValueKey(), delta.doubleValue()); + } else { + result.put(config.getOutputValueKey(), delta.longValueExact()); + } + + if (config.isAddPeriodBetweenMsgs()) { + long period = previousData != null ? currentTs - previousData.ts : 0; + result.put(config.getPeriodValueKey(), period); + } + ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(result))); + }, + t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); + } else { + ctx.tellNext(msg, "Other"); + } + } else { + ctx.tellNext(msg, "Other"); + } + } + + @Override + public void destroy() { + if (useCache) { + cache.clear(); + } + } + + private ListenableFuture fetchLatestValue(EntityId entityId) { + return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, Collections.singletonList(config.getInputValueKey())), + list -> extractValue(list.get(0)) + , ctx.getDbCallbackExecutor()); + } + + private ListenableFuture getLastValue(EntityId entityId) { + ValueWithTs latestValue; + if (useCache && (latestValue = cache.get(entityId)) != null) { + return Futures.immediateFuture(latestValue); + } else { + return fetchLatestValue(entityId); + } + } + + private ValueWithTs extractValue(TsKvEntry kvEntry) { + if (kvEntry == null || kvEntry.getValue() == null) { + return null; + } + double result = 0.0; + long ts = kvEntry.getTs(); + switch (kvEntry.getDataType()) { + case LONG: + result = kvEntry.getLongValue().get(); + break; + case DOUBLE: + result = kvEntry.getDoubleValue().get(); + break; + case STRING: + try { + result = Double.parseDouble(kvEntry.getStrValue().get()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Calculation failed. Unable to parse value [" + kvEntry.getStrValue().get() + "]" + + " of telemetry [" + kvEntry.getKey() + "] to Double"); + } + break; + case BOOLEAN: + throw new IllegalArgumentException("Calculation failed. Boolean values are not supported!"); + case JSON: + throw new IllegalArgumentException("Calculation failed. JSON values are not supported!"); + } + return new ValueWithTs(ts, result); + } + + private static class ValueWithTs { + private final long ts; + private final double value; + + private ValueWithTs(long ts, double value) { + this.ts = ts; + this.value = value; + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java new file mode 100644 index 0000000000..bec93a1d90 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2021 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.metadata; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CalculateDeltaNodeConfiguration implements NodeConfiguration { + private String inputValueKey; + private String outputValueKey; + private boolean useCache; + private boolean addPeriodBetweenMsgs; + private String periodValueKey; + private Integer round; + private boolean tellFailureIfDeltaIsNegative; + + @Override + public CalculateDeltaNodeConfiguration defaultConfiguration() { + CalculateDeltaNodeConfiguration configuration = new CalculateDeltaNodeConfiguration(); + configuration.setInputValueKey("pulseCounter"); + configuration.setOutputValueKey("delta"); + configuration.setUseCache(true); + configuration.setAddPeriodBetweenMsgs(false); + configuration.setPeriodValueKey("periodInMs"); + configuration.setTellFailureIfDeltaIsNegative(true); + return configuration; + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index 5788b2e086..0bb8cc7cfc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; @@ -61,7 +62,7 @@ public abstract class TbAbstractGetAttributesNode> failuresMap = new ConcurrentHashMap<>(); ListenableFuture> allFutures = Futures.allAsList( - putLatestTelemetry(ctx, entityId, msg, LATEST_TS, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg.getMetaData()), failuresMap), - putAttrAsync(ctx, entityId, msg, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg.getMetaData()), failuresMap, "cs_"), - putAttrAsync(ctx, entityId, msg, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg.getMetaData()), failuresMap, "shared_"), - putAttrAsync(ctx, entityId, msg, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg.getMetaData()), failuresMap, "ss_") + putLatestTelemetry(ctx, entityId, msg, LATEST_TS, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresMap), + putAttrAsync(ctx, entityId, msg, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresMap, "cs_"), + putAttrAsync(ctx, entityId, msg, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg), failuresMap, "shared_"), + putAttrAsync(ctx, entityId, msg, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresMap, "ss_") ); withCallback(allFutures, i -> { if (!failuresMap.isEmpty()) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java index 8e69c2b2c6..93d974163a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java @@ -83,11 +83,8 @@ public abstract class TbAbstractGetEntityDetailsNode getTbMsgListenableFuture(TbContext ctx, TbMsg msg, MessageData messageData, String prefix) { if (!this.config.getDetailsList().isEmpty()) { - ListenableFuture resultObject = null; ListenableFuture contactBasedListenableFuture = getContactBasedListenableFuture(ctx, msg); - for (EntityDetails entityDetails : this.config.getDetailsList()) { - resultObject = addContactProperties(messageData.getData(), contactBasedListenableFuture, entityDetails, prefix); - } + ListenableFuture resultObject = addContactProperties(messageData.getData(), contactBasedListenableFuture, prefix); return transformMsg(ctx, msg, resultObject, messageData); } else { return Futures.immediateFuture(msg); @@ -109,10 +106,14 @@ public abstract class TbAbstractGetEntityDetailsNode addContactProperties(JsonElement data, ListenableFuture entityFuture, EntityDetails entityDetails, String prefix) { + private ListenableFuture addContactProperties(JsonElement data, ListenableFuture entityFuture, String prefix) { return Futures.transformAsync(entityFuture, contactBased -> { if (contactBased != null) { - return Futures.immediateFuture(setProperties(contactBased, data, entityDetails, prefix)); + JsonElement jsonElement = null; + for (EntityDetails entityDetails : this.config.getDetailsList()) { + jsonElement = setProperties(contactBased, data, entityDetails, prefix); + } + return Futures.immediateFuture(jsonElement); } else { return Futures.immediateFuture(null); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index a2a50ff70a..e3303d074e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -105,6 +106,18 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode { + if (user != null) { + if (!user.getCustomerId().isNullUid()) { + return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), user.getCustomerId()); + } else { + throw new RuntimeException("User with name '" + user.getName() + "' is not assigned to Customer."); + } + } else { + return Futures.immediateFuture(null); + } + }, MoreExecutors.directExecutor()); default: throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported."); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java index 2ae5fd7add..b9a8df3acd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java @@ -19,7 +19,7 @@ import lombok.Data; import org.thingsboard.rule.engine.data.RelationsQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.EntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import java.util.Collections; import java.util.HashMap; @@ -41,8 +41,8 @@ public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfig RelationsQuery relationsQuery = new RelationsQuery(); relationsQuery.setDirection(EntitySearchDirection.FROM); relationsQuery.setMaxLevel(1); - EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); - relationsQuery.setFilters(Collections.singletonList(entityTypeFilter)); + RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList()); + relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter)); configuration.setRelationsQuery(relationsQuery); return configuration; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index bb5ffee78d..f484a5551c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -90,7 +91,7 @@ public class TbGetTelemetryNode implements TbNode { orderByFetchAll = ASC_ORDER; } mapper = new ObjectMapper(); - mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false); + mapper.configure(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature(), false); mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); } @@ -103,7 +104,7 @@ public class TbGetTelemetryNode implements TbNode { if (config.isUseMetadataIntervalPatterns()) { checkMetadataKeyPatterns(msg); } - List keys = TbNodeUtils.processPatterns(tsKeyNames, msg.getMetaData()); + List keys = TbNodeUtils.processPatterns(tsKeyNames, msg); ListenableFuture> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(msg, keys)); DonAsynchron.withCallback(list, data -> { process(data, msg, keys); @@ -197,10 +198,10 @@ public class TbGetTelemetryNode implements TbNode { Interval interval = new Interval(); if (config.isUseMetadataIntervalPatterns()) { if (isParsable(msg, config.getStartIntervalPattern())) { - interval.setStartTs(Long.parseLong(TbNodeUtils.processPattern(config.getStartIntervalPattern(), msg.getMetaData()))); + interval.setStartTs(Long.parseLong(TbNodeUtils.processPattern(config.getStartIntervalPattern(), msg))); } if (isParsable(msg, config.getEndIntervalPattern())) { - interval.setEndTs(Long.parseLong(TbNodeUtils.processPattern(config.getEndIntervalPattern(), msg.getMetaData()))); + interval.setEndTs(Long.parseLong(TbNodeUtils.processPattern(config.getEndIntervalPattern(), msg))); } } else { long ts = System.currentTimeMillis(); @@ -211,7 +212,7 @@ public class TbGetTelemetryNode implements TbNode { } private boolean isParsable(TbMsg msg, String pattern) { - return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg.getMetaData())); + return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg)); } private void checkMetadataKeyPatterns(TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 63fbb23fc1..4c732e0d97 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -18,22 +18,28 @@ package org.thingsboard.rule.engine.mqtt; import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; import io.netty.util.concurrent.Future; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.util.StringUtils; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttConnectResult; -import org.springframework.util.StringUtils; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; -import org.thingsboard.rule.engine.api.*; +import org.thingsboard.rule.engine.credentials.BasicCredentials; +import org.thingsboard.rule.engine.credentials.ClientCredentials; +import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.net.ssl.SSLException; import java.nio.charset.Charset; -import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -70,17 +76,17 @@ public class TbMqttNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { - String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg.getMetaData()); + String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg); this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE) .addListener(future -> { - if (future.isSuccess()) { - ctx.tellSuccess(msg); - } else { - TbMsg next = processException(ctx, msg, future.cause()); - ctx.tellFailure(next, future.cause()); - } - } - ); + if (future.isSuccess()) { + ctx.tellSuccess(msg); + } else { + TbMsg next = processException(ctx, msg, future.cause()); + ctx.tellFailure(next, future.cause()); + } + } + ); } private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { @@ -97,13 +103,13 @@ public class TbMqttNode implements TbNode { } protected MqttClient initClient(TbContext ctx) throws Exception { - Optional sslContextOpt = initSslContext(); - MqttClientConfig config = sslContextOpt.isPresent() ? new MqttClientConfig(sslContextOpt.get()) : new MqttClientConfig(); + MqttClientConfig config = new MqttClientConfig(getSslContext()); if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { config.setClientId(this.mqttNodeConfiguration.getClientId()); } config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); - this.mqttNodeConfiguration.getCredentials().configure(config); + + prepareMqttClientConfig(config); MqttClient client = MqttClient.create(config, null); client.setEventLoop(ctx.getSharedEventLoop()); Future connectFuture = client.connect(this.mqttNodeConfiguration.getHost(), this.mqttNodeConfiguration.getPort()); @@ -125,12 +131,17 @@ public class TbMqttNode implements TbNode { return client; } - private Optional initSslContext() throws SSLException { - Optional result = this.mqttNodeConfiguration.getCredentials().initSslContext(); - if (this.mqttNodeConfiguration.isSsl() && !result.isPresent()) { - result = Optional.of(SslContextBuilder.forClient().build()); + protected void prepareMqttClientConfig(MqttClientConfig config) throws SSLException { + ClientCredentials credentials = this.mqttNodeConfiguration.getCredentials(); + if (credentials.getType() == CredentialsType.BASIC) { + BasicCredentials basicCredentials = (BasicCredentials) credentials; + config.setUsername(basicCredentials.getUsername()); + config.setPassword(basicCredentials.getPassword()); } - return result; + } + + private SslContext getSslContext() throws SSLException { + return this.mqttNodeConfiguration.isSsl() ? this.mqttNodeConfiguration.getCredentials().initSslContext() : null; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java index 675fb019c5..e06824180e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java @@ -17,8 +17,8 @@ package org.thingsboard.rule.engine.mqtt; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; -import org.thingsboard.rule.engine.mqtt.credentials.AnonymousCredentials; -import org.thingsboard.rule.engine.mqtt.credentials.MqttClientCredentials; +import org.thingsboard.rule.engine.credentials.AnonymousCredentials; +import org.thingsboard.rule.engine.credentials.ClientCredentials; @Data public class TbMqttNodeConfiguration implements NodeConfiguration { @@ -31,7 +31,7 @@ public class TbMqttNodeConfiguration implements NodeConfiguration initSslContext() { + public SslContext initSslContext() { try { Security.addProvider(new BouncyCastleProvider()); if (caCert == null || caCert.isEmpty()) { caCert = AzureIotHubUtil.getDefaultCaCert(); } - return Optional.of(SslContextBuilder.forClient() + return SslContextBuilder.forClient() .trustManager(createAndInitTrustManagerFactory()) .clientAuth(ClientAuth.REQUIRE) - .build()); + .build(); } catch (Exception e) { log.error("[{}] Creating TLS factory failed!", caCert, e); throw new RuntimeException("Creating TLS factory failed!", e); @@ -60,32 +59,8 @@ public class AzureIotHubSasCredentials implements MqttClientCredentials { } @Override - public void configure(MqttClientConfig config) { + public CredentialsType getType() { + return CredentialsType.SAS; } - private TrustManagerFactory createAndInitTrustManagerFactory() throws Exception { - X509Certificate caCertHolder; - caCertHolder = readCertFile(caCert); - - KeyStore caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - caKeyStore.load(null, null); - caKeyStore.setCertificateEntry("caCert-cert", caCertHolder); - - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(caKeyStore); - return trustManagerFactory; - } - - private X509Certificate readCertFile(String fileContent) throws Exception { - X509Certificate certificate = null; - if (fileContent != null && !fileContent.trim().isEmpty()) { - fileContent = fileContent.replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replaceAll("\\s", ""); - byte[] decoded = Base64.decodeBase64(fileContent); - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - certificate = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decoded)); - } - return certificate; - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 9d233c3378..9718b10f9c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.mqtt.azure; import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.ssl.SslContext; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; import org.thingsboard.common.util.AzureIotHubUtil; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.rule.engine.api.RuleNode; @@ -25,13 +26,15 @@ 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.credentials.BasicCredentials; +import org.thingsboard.rule.engine.credentials.CertPemCredentials; +import org.thingsboard.rule.engine.credentials.ClientCredentials; +import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.rule.engine.mqtt.TbMqttNode; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; -import org.thingsboard.rule.engine.mqtt.credentials.CertPemClientCredentials; -import org.thingsboard.rule.engine.mqtt.credentials.MqttClientCredentials; import org.thingsboard.server.common.data.plugin.ComponentType; -import java.util.Optional; +import javax.net.ssl.SSLException; @Slf4j @RuleNode( @@ -50,37 +53,25 @@ public class TbAzureIotHubNode extends TbMqttNode { this.mqttNodeConfiguration = TbNodeUtils.convert(configuration, TbMqttNodeConfiguration.class); mqttNodeConfiguration.setPort(8883); mqttNodeConfiguration.setCleanSession(true); - MqttClientCredentials credentials = mqttNodeConfiguration.getCredentials(); - mqttNodeConfiguration.setCredentials(new MqttClientCredentials() { - @Override - public Optional initSslContext() { - if (credentials instanceof AzureIotHubSasCredentials) { - AzureIotHubSasCredentials sasCredentials = (AzureIotHubSasCredentials) credentials; - if (sasCredentials.getCaCert() == null || sasCredentials.getCaCert().isEmpty()) { - sasCredentials.setCaCert(AzureIotHubUtil.getDefaultCaCert()); - } - } else if (credentials instanceof CertPemClientCredentials) { - CertPemClientCredentials pemCredentials = (CertPemClientCredentials) credentials; - if (pemCredentials.getCaCert() == null || pemCredentials.getCaCert().isEmpty()) { - pemCredentials.setCaCert(AzureIotHubUtil.getDefaultCaCert()); - } - } - return credentials.initSslContext(); + ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); + if (CredentialsType.CERT_PEM == credentials.getType()) { + CertPemCredentials pemCredentials = (CertPemCredentials) credentials; + if (pemCredentials.getCaCert() == null || pemCredentials.getCaCert().isEmpty()) { + pemCredentials.setCaCert(AzureIotHubUtil.getDefaultCaCert()); } - - @Override - public void configure(MqttClientConfig config) { - config.setProtocolVersion(MqttVersion.MQTT_3_1_1); - config.setUsername(AzureIotHubUtil.buildUsername(mqttNodeConfiguration.getHost(), config.getClientId())); - if (credentials instanceof AzureIotHubSasCredentials) { - AzureIotHubSasCredentials sasCredentials = (AzureIotHubSasCredentials) credentials; - config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), sasCredentials.getSasKey())); - } - } - }); - + } this.mqttClient = initClient(ctx); } catch (Exception e) { throw new TbNodeException(e); - } } + } + } + + protected void prepareMqttClientConfig(MqttClientConfig config) throws SSLException { + config.setProtocolVersion(MqttVersion.MQTT_3_1_1); + config.setUsername(AzureIotHubUtil.buildUsername(mqttNodeConfiguration.getHost(), config.getClientId())); + ClientCredentials credentials = mqttNodeConfiguration.getCredentials(); + if (CredentialsType.SAS == credentials.getType()) { + config.setPassword(AzureIotHubUtil.buildSasToken(mqttNodeConfiguration.getHost(), ((AzureIotHubSasCredentials) credentials).getSasKey())); + } + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeConfiguration.java index ebc88768af..5da95b30d1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNodeConfiguration.java @@ -16,10 +16,7 @@ package org.thingsboard.rule.engine.mqtt.azure; import lombok.Data; -import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; -import org.thingsboard.rule.engine.mqtt.credentials.AnonymousCredentials; -import org.thingsboard.rule.engine.mqtt.credentials.MqttClientCredentials; @Data public class TbAzureIotHubNodeConfiguration extends TbMqttNodeConfiguration { 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 51542132fc..ab0d9df7c2 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 @@ -16,9 +16,13 @@ package org.thingsboard.rule.engine.profile; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.profile.state.PersistedAlarmRuleState; 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.AlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule; @@ -45,6 +49,7 @@ import java.util.Set; import java.util.function.Function; @Data +@Slf4j class AlarmRuleState { private final AlarmSeverity severity; @@ -52,11 +57,12 @@ class AlarmRuleState { private final AlarmConditionSpec spec; private final long requiredDurationInMs; private final long requiredRepeats; - private final Set entityKeys; + private final Set entityKeys; private PersistedAlarmRuleState state; private boolean updateFlag; + private final DynamicPredicateValueCtx dynamicPredicateValueCtx; - AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, Set entityKeys, PersistedAlarmRuleState state) { + AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, Set entityKeys, PersistedAlarmRuleState state, DynamicPredicateValueCtx dynamicPredicateValueCtx) { this.severity = severity; this.alarmRule = alarmRule; this.entityKeys = entityKeys; @@ -80,10 +86,11 @@ class AlarmRuleState { } this.requiredDurationInMs = requiredDurationInMs; this.requiredRepeats = requiredRepeats; + this.dynamicPredicateValueCtx = dynamicPredicateValueCtx; } - public boolean validateTsUpdate(Set changedKeys) { - for (EntityKey key : changedKeys) { + public boolean validateTsUpdate(Set changedKeys) { + for (AlarmConditionFilterKey key : changedKeys) { if (entityKeys.contains(key)) { return true; } @@ -91,18 +98,14 @@ class AlarmRuleState { return false; } - public boolean validateAttrUpdate(Set changedKeys) { + public boolean validateAttrUpdate(Set changedKeys) { //If the attribute was updated, but no new telemetry arrived - we ignore this until new telemetry is there. - for (EntityKey key : entityKeys) { - if (key.getType().equals(EntityKeyType.TIME_SERIES)) { + for (AlarmConditionFilterKey key : entityKeys) { + if (key.getType().equals(AlarmConditionKeyType.TIME_SERIES)) { return false; } } - for (EntityKey key : changedKeys) { - EntityKeyType keyType = key.getType(); - if (EntityKeyType.CLIENT_ATTRIBUTE.equals(keyType) || EntityKeyType.SERVER_ATTRIBUTE.equals(keyType) || EntityKeyType.SHARED_ATTRIBUTE.equals(keyType)) { - key = new EntityKey(EntityKeyType.ATTRIBUTE, key.getKey()); - } + for (AlarmConditionFilterKey key : changedKeys) { if (entityKeys.contains(key)) { return true; } @@ -257,16 +260,46 @@ class AlarmRuleState { private boolean eval(AlarmCondition condition, DataSnapshot data) { boolean eval = true; - for (KeyFilter keyFilter : condition.getCondition()) { - EntityKeyValue value = data.getValue(keyFilter.getKey()); + for (var filter : condition.getCondition()) { + EntityKeyValue value; + if (filter.getKey().getType().equals(AlarmConditionKeyType.CONSTANT)) { + try { + value = getConstantValue(filter); + } catch (RuntimeException e) { + log.warn("Failed to parse constant value from filter: {}", filter, e); + value = null; + } + } else { + value = data.getValue(filter.getKey()); + } if (value == null) { return false; } - eval = eval && eval(data, value, keyFilter.getPredicate()); + eval = eval && eval(data, value, filter.getPredicate()); } return eval; } + private EntityKeyValue getConstantValue(AlarmConditionFilter filter) { + EntityKeyValue value = new EntityKeyValue(); + String valueStr = filter.getValue().toString(); + switch (filter.getValueType()) { + case STRING: + value.setStrValue(valueStr); + break; + case DATE_TIME: + value.setLngValue(Long.valueOf(valueStr)); + break; + case NUMERIC: + value.setDblValue(Double.valueOf(valueStr)); + break; + case BOOLEAN: + value.setBoolValue(Boolean.valueOf(valueStr)); + break; + } + return value; + } + private boolean eval(DataSnapshot data, EntityKeyValue value, KeyFilterPredicate predicate) { switch (predicate.getType()) { case STRING: @@ -385,15 +418,19 @@ class AlarmRuleState { private EntityKeyValue getDynamicPredicateValue(DataSnapshot data, FilterPredicateValue value) { EntityKeyValue ekv = null; if (value.getDynamicValue() != null) { - ekv = data.getValue(new EntityKey(EntityKeyType.ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); - if (ekv == null) { - ekv = data.getValue(new EntityKey(EntityKeyType.SERVER_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); - if (ekv == null) { - ekv = data.getValue(new EntityKey(EntityKeyType.SHARED_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); - if (ekv == null) { - ekv = data.getValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); + switch (value.getDynamicValue().getSourceType()) { + case CURRENT_DEVICE: + ekv = data.getValue(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, value.getDynamicValue().getSourceAttribute())); + if (ekv != null || !value.getDynamicValue().isInherit()) { + break; } - } + case CURRENT_CUSTOMER: + ekv = dynamicPredicateValueCtx.getCustomerValue(value.getDynamicValue().getSourceAttribute()); + if (ekv != null || !value.getDynamicValue().isInherit()) { + break; + } + case CURRENT_TENANT: + ekv = dynamicPredicateValueCtx.getTenantValue(value.getDynamicValue().getSourceAttribute()); } } return ekv; 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 ed613dd55f..78092f5473 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 @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.query.EntityKeyType; @@ -35,7 +36,7 @@ import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceQueue; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import java.util.ArrayList; import java.util.Comparator; @@ -57,10 +58,12 @@ class AlarmState { private volatile TbMsgMetaData lastMsgMetaData; private volatile String lastMsgQueueName; private volatile DataSnapshot dataSnapshot; + private final DynamicPredicateValueCtx dynamicPredicateValueCtx; - AlarmState(ProfileState deviceProfile, EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState) { + AlarmState(ProfileState deviceProfile, EntityId originator, DeviceProfileAlarm alarmDefinition, PersistedAlarmState alarmState, DynamicPredicateValueCtx dynamicPredicateValueCtx) { this.deviceProfile = deviceProfile; this.originator = originator; + this.dynamicPredicateValueCtx = dynamicPredicateValueCtx; this.updateState(alarmDefinition, alarmState); } @@ -133,9 +136,9 @@ class AlarmState { public boolean validateUpdate(SnapshotUpdate update, AlarmRuleState state) { if (update != null) { //Check that the update type and that keys match. - if (update.getType().equals(EntityKeyType.TIME_SERIES)) { + if (update.getType().equals(AlarmConditionKeyType.TIME_SERIES)) { return state.validateTsUpdate(update.getKeys()); - } else if (update.getType().equals(EntityKeyType.ATTRIBUTE)) { + } else if (update.getType().equals(AlarmConditionKeyType.ATTRIBUTE)) { return state.validateAttrUpdate(update.getKeys()); } } @@ -188,12 +191,12 @@ class AlarmState { } } createRulesSortedBySeverityDesc.add(new AlarmRuleState(severity, rule, - deviceProfile.getCreateAlarmKeys(alarm.getId(), severity), ruleState)); + deviceProfile.getCreateAlarmKeys(alarm.getId(), severity), ruleState, dynamicPredicateValueCtx)); }); createRulesSortedBySeverityDesc.sort(Comparator.comparingInt(state -> state.getSeverity().ordinal())); PersistedAlarmRuleState ruleState = alarmState == null ? null : alarmState.getClearRuleState(); if (alarmDefinition.getClearRule() != null) { - clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), deviceProfile.getClearAlarmKeys(alarm.getId()), ruleState); + clearState = new AlarmRuleState(null, alarmDefinition.getClearRule(), deviceProfile.getClearAlarmKeys(alarm.getId()), ruleState, dynamicPredicateValueCtx); } } @@ -223,7 +226,11 @@ class AlarmState { currentAlarm.setType(alarmDefinition.getAlarmType()); currentAlarm.setStatus(AlarmStatus.ACTIVE_UNACK); currentAlarm.setSeverity(severity); - currentAlarm.setStartTs(System.currentTimeMillis()); + long startTs = dataSnapshot.getTs(); + if (startTs == 0L) { + startTs = System.currentTimeMillis(); + } + currentAlarm.setStartTs(startTs); currentAlarm.setEndTs(currentAlarm.getStartTs()); currentAlarm.setDetails(createDetails(ruleState)); currentAlarm.setOriginator(originator); @@ -243,7 +250,7 @@ class AlarmState { String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); if (StringUtils.isNotEmpty(alarmDetailsStr)) { - for (KeyFilter keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { + for (var keyFilter : ruleState.getAlarmRule().getCondition().getCondition()) { EntityKeyValue entityKeyValue = dataSnapshot.getValue(keyFilter.getKey()); alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", keyFilter.getKey().getKey()), getValueAsString(entityKeyValue)); } @@ -291,4 +298,11 @@ class AlarmState { } return updated; } + + public void processAckAlarm(Alarm alarm) { + if (currentAlarm != null && currentAlarm.getId().equals(alarm.getId())) { + currentAlarm.setStatus(alarm.getStatus()); + currentAlarm.setAckTs(alarm.getAckTs()); + } + } } 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 ed939bc3c1..c7db043dc9 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 @@ -17,6 +17,8 @@ package org.thingsboard.rule.engine.profile; import lombok.Getter; import lombok.Setter; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; +import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; @@ -30,55 +32,42 @@ class DataSnapshot { @Getter @Setter private long ts; - private final Set keys; - private final Map values = new ConcurrentHashMap<>(); + private final Set keys; + private final Map values = new ConcurrentHashMap<>(); - DataSnapshot(Set entityKeysToFetch) { + DataSnapshot(Set entityKeysToFetch) { this.keys = entityKeysToFetch; } - void removeValue(EntityKey key) { - switch (key.getType()) { - case ATTRIBUTE: - values.remove(key); - values.remove(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE)); - values.remove(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE)); - values.remove(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE)); - break; - case CLIENT_ATTRIBUTE: - case SHARED_ATTRIBUTE: - case SERVER_ATTRIBUTE: - values.remove(key); - values.remove(getAttrKey(key, EntityKeyType.ATTRIBUTE)); - break; - default: - values.remove(key); - } + static AlarmConditionFilterKey toConditionKey(EntityKey key) { + return new AlarmConditionFilterKey(toConditionKeyType(key.getType()), key.getKey()); } - boolean putValue(EntityKey key, long newTs, EntityKeyValue value) { - boolean updateOfTs = ts != newTs; - boolean result = false; - switch (key.getType()) { + static AlarmConditionKeyType toConditionKeyType(EntityKeyType keyType) { + switch (keyType) { case ATTRIBUTE: - result |= putIfKeyExists(key, value, updateOfTs); - result |= putIfKeyExists(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE), value, updateOfTs); - result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE), value, updateOfTs); - result |= putIfKeyExists(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE), value, updateOfTs); - break; - case CLIENT_ATTRIBUTE: - case SHARED_ATTRIBUTE: case SERVER_ATTRIBUTE: - result |= putIfKeyExists(key, value, updateOfTs); - result |= putIfKeyExists(getAttrKey(key, EntityKeyType.ATTRIBUTE), value, updateOfTs); - break; + case SHARED_ATTRIBUTE: + case CLIENT_ATTRIBUTE: + return AlarmConditionKeyType.ATTRIBUTE; + case TIME_SERIES: + return AlarmConditionKeyType.TIME_SERIES; + case ENTITY_FIELD: + return AlarmConditionKeyType.ENTITY_FIELD; default: - result |= putIfKeyExists(key, value, updateOfTs); + throw new RuntimeException("Not supported entity key: " + keyType.name()); } - return result; } - private boolean putIfKeyExists(EntityKey key, EntityKeyValue value, boolean updateOfTs) { + void removeValue(EntityKey key) { + values.remove(toConditionKey(key)); + } + + boolean putValue(AlarmConditionFilterKey key, long newTs, EntityKeyValue value) { + return putIfKeyExists(key, value, ts != newTs); + } + + private boolean putIfKeyExists(AlarmConditionFilterKey key, EntityKeyValue value, boolean updateOfTs) { if (keys.contains(key)) { EntityKeyValue oldValue = values.put(key, value); if (updateOfTs) { @@ -91,25 +80,7 @@ class DataSnapshot { } } - EntityKeyValue getValue(EntityKey key) { - if (EntityKeyType.ATTRIBUTE.equals(key.getType())) { - EntityKeyValue value = values.get(key); - if (value == null) { - value = values.get(getAttrKey(key, EntityKeyType.CLIENT_ATTRIBUTE)); - if (value == null) { - value = values.get(getAttrKey(key, EntityKeyType.SHARED_ATTRIBUTE)); - if (value == null) { - value = values.get(getAttrKey(key, EntityKeyType.SERVER_ATTRIBUTE)); - } - } - } - return value; - } else { - return values.get(key); - } - } - - private EntityKey getAttrKey(EntityKey key, EntityKeyType clientAttribute) { - return new EntityKey(clientAttribute, key.getKey()); + EntityKeyValue getValue(AlarmConditionFilterKey key) { + return values.get(key); } } 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 e078df6156..e84beae702 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 @@ -26,6 +26,8 @@ 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.alarm.Alarm; +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.DeviceProfileAlarm; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -40,7 +42,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.dao.sql.query.EntityKeyMapping; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import java.util.ArrayList; import java.util.HashMap; @@ -63,11 +65,15 @@ class DeviceState { private PersistedDeviceState pds; private DataSnapshot latestValues; private final ConcurrentMap alarmStates = new ConcurrentHashMap<>(); + private final DynamicPredicateValueCtx dynamicPredicateValueCtx; DeviceState(TbContext ctx, TbDeviceProfileNodeConfiguration config, DeviceId deviceId, ProfileState deviceProfile, RuleNodeState state) { this.persistState = config.isPersistAlarmRulesState(); this.deviceId = deviceId; this.deviceProfile = deviceProfile; + + this.dynamicPredicateValueCtx = new DynamicPredicateValueCtxImpl(ctx.getTenantId(), deviceId, ctx); + if (config.isPersistAlarmRulesState()) { if (state != null) { this.state = state; @@ -87,16 +93,16 @@ class DeviceState { if (pds != null) { for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { alarmStates.computeIfAbsent(alarm.getId(), - a -> new AlarmState(deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + a -> new AlarmState(deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); } } } public void updateProfile(TbContext ctx, DeviceProfile deviceProfile) throws ExecutionException, InterruptedException { - Set oldKeys = this.deviceProfile.getEntityKeys(); + Set oldKeys = this.deviceProfile.getEntityKeys(); this.deviceProfile.updateDeviceProfile(deviceProfile); if (latestValues != null) { - Set keysToFetch = new HashSet<>(this.deviceProfile.getEntityKeys()); + Set keysToFetch = new HashSet<>(this.deviceProfile.getEntityKeys()); keysToFetch.removeAll(oldKeys); if (!keysToFetch.isEmpty()) { addEntityKeysToSnapshot(ctx, deviceId, keysToFetch, latestValues); @@ -108,7 +114,7 @@ class DeviceState { if (alarmStates.containsKey(alarm.getId())) { alarmStates.get(alarm.getId()).updateState(alarm, getOrInitPersistedAlarmState(alarm)); } else { - alarmStates.putIfAbsent(alarm.getId(), new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + alarmStates.putIfAbsent(alarm.getId(), new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); } } } @@ -134,13 +140,20 @@ class DeviceState { stateChanged = processTelemetry(ctx, msg); } else if (msg.getType().equals(SessionMsgType.POST_ATTRIBUTES_REQUEST.name())) { stateChanged = processAttributesUpdateRequest(ctx, msg); + } else if (msg.getType().equals(DataConstants.ACTIVITY_EVENT) || msg.getType().equals(DataConstants.INACTIVITY_EVENT)) { + stateChanged = processDeviceActivityEvent(ctx, msg); } else if (msg.getType().equals(DataConstants.ATTRIBUTES_UPDATED)) { stateChanged = processAttributesUpdateNotification(ctx, msg); } else if (msg.getType().equals(DataConstants.ATTRIBUTES_DELETED)) { stateChanged = processAttributesDeleteNotification(ctx, msg); } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { stateChanged = processAlarmClearNotification(ctx, msg); + } else if (msg.getType().equals(DataConstants.ALARM_ACK)) { + processAlarmAckNotification(ctx, msg); } else { + if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED) || msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) { + dynamicPredicateValueCtx.resetCustomer(); + } ctx.tellSuccess(msg); } if (persistState && stateChanged) { @@ -149,32 +162,50 @@ class DeviceState { } } + private boolean processDeviceActivityEvent(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { + String scope = msg.getMetaData().getValue(DataConstants.SCOPE); + if (StringUtils.isEmpty(scope)) { + return processTelemetry(ctx, msg); + } else { + return processAttributes(ctx, msg, scope); + } + } + private boolean processAlarmClearNotification(TbContext ctx, TbMsg msg) { boolean stateChanged = false; Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); stateChanged |= alarmState.processAlarmClear(ctx, alarmNf); } ctx.tellSuccess(msg); return stateChanged; } + private void processAlarmAckNotification(TbContext ctx, TbMsg msg) { + Alarm alarmNf = JacksonUtil.fromString(msg.getData(), Alarm.class); + for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { + AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); + alarmState.processAckAlarm(alarmNf); + } + ctx.tellSuccess(msg); + } + private boolean processAttributesUpdateNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { - Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); - String scope = msg.getMetaData().getValue("scope"); + String scope = msg.getMetaData().getValue(DataConstants.SCOPE); if (StringUtils.isEmpty(scope)) { scope = DataConstants.CLIENT_SCOPE; } - return processAttributesUpdate(ctx, msg, attributes, scope); + return processAttributes(ctx, msg, scope); } private boolean processAttributesDeleteNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { boolean stateChanged = false; List keys = new ArrayList<>(); new JsonParser().parse(msg.getData()).getAsJsonObject().get("attributes").getAsJsonArray().forEach(e -> keys.add(e.getAsString())); - String scope = msg.getMetaData().getValue("scope"); + String scope = msg.getMetaData().getValue(DataConstants.SCOPE); if (StringUtils.isEmpty(scope)) { scope = DataConstants.CLIENT_SCOPE; } @@ -183,7 +214,7 @@ class DeviceState { keys.forEach(key -> latestValues.removeValue(new EntityKey(keyType, key))); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); stateChanged |= alarmState.process(ctx, msg, latestValues, null); } } @@ -192,17 +223,17 @@ class DeviceState { } protected boolean processAttributesUpdateRequest(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { - Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); - return processAttributesUpdate(ctx, msg, attributes, DataConstants.CLIENT_SCOPE); + return processAttributes(ctx, msg, DataConstants.CLIENT_SCOPE); } - private boolean processAttributesUpdate(TbContext ctx, TbMsg msg, Set attributes, String scope) throws ExecutionException, InterruptedException { + private boolean processAttributes(TbContext ctx, TbMsg msg, String scope) throws ExecutionException, InterruptedException { boolean stateChanged = false; + Set attributes = JsonConverter.convertToAttributes(new JsonParser().parse(msg.getData())); if (!attributes.isEmpty()) { SnapshotUpdate update = merge(latestValues, attributes, scope); for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); stateChanged |= alarmState.process(ctx, msg, latestValues, update); } } @@ -221,7 +252,7 @@ class DeviceState { if (update.hasUpdate()) { for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), - a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm))); + a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); stateChanged |= alarmState.process(ctx, msg, latestValues, update); } } @@ -231,29 +262,29 @@ class DeviceState { } private SnapshotUpdate merge(DataSnapshot latestValues, Long newTs, List data) { - Set keys = new HashSet<>(); + Set keys = new HashSet<>(); for (KvEntry entry : data) { - EntityKey entityKey = new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()); + AlarmConditionFilterKey entityKey = new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, entry.getKey()); if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) { keys.add(entityKey); } } latestValues.setTs(newTs); - return new SnapshotUpdate(EntityKeyType.TIME_SERIES, keys); + return new SnapshotUpdate(AlarmConditionKeyType.TIME_SERIES, keys); } private SnapshotUpdate merge(DataSnapshot latestValues, Set attributes, String scope) { long newTs = 0; - Set keys = new HashSet<>(); + Set keys = new HashSet<>(); for (AttributeKvEntry entry : attributes) { newTs = Math.max(newTs, entry.getLastUpdateTs()); - EntityKey entityKey = new EntityKey(getKeyTypeFromScope(scope), entry.getKey()); + AlarmConditionFilterKey entityKey = new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, entry.getKey()); if (latestValues.putValue(entityKey, newTs, toEntityValue(entry))) { keys.add(entityKey); } } latestValues.setTs(newTs); - return new SnapshotUpdate(EntityKeyType.ATTRIBUTE, keys); + return new SnapshotUpdate(AlarmConditionKeyType.ATTRIBUTE, keys); } private static EntityKeyType getKeyTypeFromScope(String scope) { @@ -269,37 +300,22 @@ class DeviceState { } private DataSnapshot fetchLatestValues(TbContext ctx, EntityId originator) throws ExecutionException, InterruptedException { - Set entityKeysToFetch = deviceProfile.getEntityKeys(); + Set entityKeysToFetch = deviceProfile.getEntityKeys(); DataSnapshot result = new DataSnapshot(entityKeysToFetch); addEntityKeysToSnapshot(ctx, originator, entityKeysToFetch, result); return result; } - private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set entityKeysToFetch, DataSnapshot result) throws InterruptedException, ExecutionException { - Set serverAttributeKeys = new HashSet<>(); - Set clientAttributeKeys = new HashSet<>(); - Set sharedAttributeKeys = new HashSet<>(); - Set commonAttributeKeys = new HashSet<>(); + private void addEntityKeysToSnapshot(TbContext ctx, EntityId originator, Set entityKeysToFetch, DataSnapshot result) throws InterruptedException, ExecutionException { + Set attributeKeys = new HashSet<>(); Set latestTsKeys = new HashSet<>(); Device device = null; - for (EntityKey entityKey : entityKeysToFetch) { + for (AlarmConditionFilterKey entityKey : entityKeysToFetch) { String key = entityKey.getKey(); switch (entityKey.getType()) { - case SERVER_ATTRIBUTE: - serverAttributeKeys.add(key); - break; - case CLIENT_ATTRIBUTE: - clientAttributeKeys.add(key); - break; - case SHARED_ATTRIBUTE: - sharedAttributeKeys.add(key); - break; case ATTRIBUTE: - serverAttributeKeys.add(key); - clientAttributeKeys.add(key); - sharedAttributeKeys.add(key); - commonAttributeKeys.add(key); + attributeKeys.add(key); break; case TIME_SERIES: latestTsKeys.add(key); @@ -332,37 +348,27 @@ class DeviceState { List data = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), originator, latestTsKeys).get(); for (TsKvEntry entry : data) { if (entry.getValue() != null) { - result.putValue(new EntityKey(EntityKeyType.TIME_SERIES, entry.getKey()), entry.getTs(), toEntityValue(entry)); + result.putValue(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, entry.getKey()), entry.getTs(), toEntityValue(entry)); } } } - if (!clientAttributeKeys.isEmpty()) { - addToSnapshot(result, commonAttributeKeys, - ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.CLIENT_SCOPE, clientAttributeKeys).get()); - } - if (!sharedAttributeKeys.isEmpty()) { - addToSnapshot(result, commonAttributeKeys, - ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SHARED_SCOPE, sharedAttributeKeys).get()); - } - if (!serverAttributeKeys.isEmpty()) { - addToSnapshot(result, commonAttributeKeys, - ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SERVER_SCOPE, serverAttributeKeys).get()); + if (!attributeKeys.isEmpty()) { + addToSnapshot(result, ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.CLIENT_SCOPE, attributeKeys).get()); + addToSnapshot(result, ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SHARED_SCOPE, attributeKeys).get()); + addToSnapshot(result, ctx.getAttributesService().find(ctx.getTenantId(), originator, DataConstants.SERVER_SCOPE, attributeKeys).get()); } } - private void addToSnapshot(DataSnapshot snapshot, Set commonAttributeKeys, List data) { + private void addToSnapshot(DataSnapshot snapshot, List data) { for (AttributeKvEntry entry : data) { if (entry.getValue() != null) { EntityKeyValue value = toEntityValue(entry); - snapshot.putValue(new EntityKey(EntityKeyType.CLIENT_ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); - if (commonAttributeKeys.contains(entry.getKey())) { - snapshot.putValue(new EntityKey(EntityKeyType.ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); - } + snapshot.putValue(new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, entry.getKey()), entry.getLastUpdateTs(), value); } } } - private EntityKeyValue toEntityValue(KvEntry entry) { + public static EntityKeyValue toEntityValue(KvEntry entry) { switch (entry.getDataType()) { case STRING: return EntityKeyValue.fromString(entry.getStrValue().get()); 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 new file mode 100644 index 0000000000..3e8df05d4a --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtx.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2021 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.profile; + +public interface DynamicPredicateValueCtx { + + EntityKeyValue getTenantValue(String key); + + EntityKeyValue getCustomerValue(String key); + + void resetCustomer(); +} 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 new file mode 100644 index 0000000000..18140040c7 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DynamicPredicateValueCtxImpl.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2021 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.profile; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +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.kv.AttributeKvEntry; + +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +@Slf4j +public class DynamicPredicateValueCtxImpl implements DynamicPredicateValueCtx { + private final TenantId tenantId; + private CustomerId customerId; + private final DeviceId deviceId; + private final TbContext ctx; + + public DynamicPredicateValueCtxImpl(TenantId tenantId, DeviceId deviceId, TbContext ctx) { + this.tenantId = tenantId; + this.deviceId = deviceId; + this.ctx = ctx; + resetCustomer(); + } + + @Override + public EntityKeyValue getTenantValue(String key) { + return getValue(tenantId, key); + } + + @Override + public EntityKeyValue getCustomerValue(String key) { + return customerId == null || customerId.isNullUid() ? null : getValue(customerId, key); + } + + @Override + public void resetCustomer() { + Device device = ctx.getDeviceService().findDeviceById(tenantId, deviceId); + if (device != null) { + this.customerId = device.getCustomerId(); + } + } + + private EntityKeyValue getValue(EntityId entityId, String key) { + try { + Optional entry = ctx.getAttributesService().find(tenantId, entityId, DataConstants.SERVER_SCOPE, key).get(); + if (entry.isPresent()) { + return DeviceState.toEntityValue(entry.get()); + } + } catch (InterruptedException | ExecutionException e) { + log.warn("Failed to get attribute by key: {} for {}: [{}]", key, entityId.getEntityType(), entityId.getId()); + } + return null; + } +} 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 7e7f407031..8fbacc120d 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 @@ -19,6 +19,9 @@ import lombok.AccessLevel; import lombok.Getter; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +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.id.DeviceProfileId; @@ -50,10 +53,10 @@ class ProfileState { @Getter(AccessLevel.PACKAGE) private final List alarmSettings = new CopyOnWriteArrayList<>(); @Getter(AccessLevel.PACKAGE) - private final Set entityKeys = ConcurrentHashMap.newKeySet(); + private final Set entityKeys = ConcurrentHashMap.newKeySet(); - private final Map>> alarmCreateKeys = new HashMap<>(); - private final Map> alarmClearKeys = new HashMap<>(); + private final Map>> alarmCreateKeys = new HashMap<>(); + private final Map> alarmClearKeys = new HashMap<>(); ProfileState(DeviceProfile deviceProfile) { updateDeviceProfile(deviceProfile); @@ -68,18 +71,18 @@ class ProfileState { if (deviceProfile.getProfileData().getAlarms() != null) { alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { - Map> createAlarmKeys = alarmCreateKeys.computeIfAbsent(alarm.getId(), id -> new HashMap<>()); + Map> createAlarmKeys = alarmCreateKeys.computeIfAbsent(alarm.getId(), id -> new HashMap<>()); alarm.getCreateRules().forEach(((severity, alarmRule) -> { - Set ruleKeys = createAlarmKeys.computeIfAbsent(severity, id -> new HashSet<>()); - for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { + var ruleKeys = createAlarmKeys.computeIfAbsent(severity, id -> new HashSet<>()); + for (var keyFilter : alarmRule.getCondition().getCondition()) { entityKeys.add(keyFilter.getKey()); ruleKeys.add(keyFilter.getKey()); addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys); } })); if (alarm.getClearRule() != null) { - Set clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>()); - for (KeyFilter keyFilter : alarm.getClearRule().getCondition().getCondition()) { + var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>()); + for (var keyFilter : alarm.getClearRule().getCondition().getCondition()) { entityKeys.add(keyFilter.getKey()); clearAlarmKeys.add(keyFilter.getKey()); addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, clearAlarmKeys); @@ -89,14 +92,16 @@ class ProfileState { } } - private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set entityKeys, Set ruleKeys) { + private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set entityKeys, Set ruleKeys) { switch (predicate.getType()) { case STRING: case NUMERIC: case BOOLEAN: DynamicValue value = ((SimpleKeyFilterPredicate) predicate).getValue().getDynamicValue(); - if (value != null && value.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE) { - EntityKey entityKey = new EntityKey(EntityKeyType.ATTRIBUTE, value.getSourceAttribute()); + if (value != null && (value.getSourceType() == DynamicValueSourceType.CURRENT_TENANT || + value.getSourceType() == DynamicValueSourceType.CURRENT_CUSTOMER || + value.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE)) { + AlarmConditionFilterKey entityKey = new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE, value.getSourceAttribute()); entityKeys.add(entityKey); ruleKeys.add(entityKey); } @@ -113,12 +118,12 @@ class ProfileState { return deviceProfile.getId(); } - Set getCreateAlarmKeys(String id, AlarmSeverity severity) { - Map> sKeys = alarmCreateKeys.get(id); + Set getCreateAlarmKeys(String id, AlarmSeverity severity) { + Map> sKeys = alarmCreateKeys.get(id); if (sKeys == null) { return Collections.emptySet(); } else { - Set keys = sKeys.get(severity); + Set keys = sKeys.get(severity); if (keys == null) { return Collections.emptySet(); } else { @@ -127,8 +132,8 @@ class ProfileState { } } - Set getClearAlarmKeys(String id) { - Set keys = alarmClearKeys.get(id); + Set getClearAlarmKeys(String id) { + Set keys = alarmClearKeys.get(id); if (keys == null) { return Collections.emptySet(); } else { 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 1ad1860d5a..7a3ce5debc 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 @@ -16,6 +16,8 @@ package org.thingsboard.rule.engine.profile; import lombok.Getter; +import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; +import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; @@ -24,11 +26,11 @@ import java.util.Set; class SnapshotUpdate { @Getter - private final EntityKeyType type; + private final AlarmConditionKeyType type; @Getter - private final Set keys; + private final Set keys; - SnapshotUpdate(EntityKeyType type, Set keys) { + SnapshotUpdate(AlarmConditionKeyType type, Set keys) { this.type = type; this.keys = keys; } 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 3faac79420..a733fd104a 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 @@ -38,7 +38,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; +import org.thingsboard.common.util.JacksonUtil; import java.util.Map; import java.util.UUID; @@ -134,7 +134,8 @@ public class TbDeviceProfileNode implements TbNode { if (deviceState != null) { deviceState.process(ctx, msg); } else { - ctx.tellFailure(msg, new IllegalStateException("Device profile for device [" + deviceId + "] not found!")); + log.info("Device was not found! Most probably device [" + deviceId + "] has been removed from the database. Acknowledging msg."); + ctx.ack(msg); } } } else { 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 3e03da287c..dac49c25cd 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 @@ -16,31 +16,8 @@ package org.thingsboard.rule.engine.profile; import lombok.Data; -import lombok.extern.slf4j.Slf4j; import org.codehaus.jackson.annotate.JsonIgnoreProperties; -import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration; -import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; -import org.thingsboard.rule.engine.api.RuleNode; -import org.thingsboard.rule.engine.api.TbContext; -import org.thingsboard.rule.engine.api.TbNode; -import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.DataConstants; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.DeviceProfileId; -import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.util.mapping.JacksonUtil; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; @Data @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index 898334781e..f481c875a9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -90,11 +90,11 @@ public class TbRabbitMqNode implements TbNode { private TbMsg publishMessage(TbContext ctx, TbMsg msg) throws Exception { String exchangeName = ""; if (!StringUtils.isEmpty(this.config.getExchangeNamePattern())) { - exchangeName = TbNodeUtils.processPattern(this.config.getExchangeNamePattern(), msg.getMetaData()); + exchangeName = TbNodeUtils.processPattern(this.config.getExchangeNamePattern(), msg); } String routingKey = ""; if (!StringUtils.isEmpty(this.config.getRoutingKeyPattern())) { - routingKey = TbNodeUtils.processPattern(this.config.getRoutingKeyPattern(), msg.getMetaData()); + routingKey = TbNodeUtils.processPattern(this.config.getRoutingKeyPattern(), msg); } AMQP.BasicProperties properties = null; if (!StringUtils.isEmpty(this.config.getMessageProperties())) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 562a6b9709..c485489954 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -17,9 +17,9 @@ package org.thingsboard.rule.engine.rest; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.handler.ssl.SslContextBuilder; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Base64; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -44,6 +44,9 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbRelationTypes; import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.rule.engine.credentials.BasicCredentials; +import org.thingsboard.rule.engine.credentials.ClientCredentials; +import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -51,6 +54,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import java.net.Authenticator; import java.net.PasswordAuthentication; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.util.Deque; import java.util.concurrent.ConcurrentLinkedDeque; @@ -58,6 +62,7 @@ import java.util.concurrent.TimeUnit; @Data @Slf4j +@SuppressWarnings("deprecation") public class TbHttpClient { private static final String STATUS = "status"; @@ -129,11 +134,14 @@ public class TbHttpClient { requestFactory.setReadTimeout(config.getReadTimeoutMs()); httpClient = new AsyncRestTemplate(requestFactory); } else if (config.isUseSimpleClientHttpFactory()) { + if (CredentialsType.CERT_PEM == config.getCredentials().getType()) { + throw new TbNodeException("Simple HTTP Factory does not support CERT PEM credentials!"); + } httpClient = new AsyncRestTemplate(); } else { this.eventLoopGroup = new NioEventLoopGroup(); Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup); - nettyFactory.setSslContext(SslContextBuilder.forClient().build()); + nettyFactory.setSslContext(config.getCredentials().initSslContext()); nettyFactory.setReadTimeout(config.getReadTimeoutMs()); httpClient = new AsyncRestTemplate(nettyFactory); } @@ -163,8 +171,8 @@ public class TbHttpClient { } public void processMessage(TbContext ctx, TbMsg msg) { - String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData()); - HttpHeaders headers = prepareHeaders(msg.getMetaData()); + String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg); + HttpHeaders headers = prepareHeaders(msg); HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); HttpEntity entity = new HttpEntity<>(msg.getData(), headers); @@ -223,9 +231,16 @@ public class TbHttpClient { return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData()); } - private HttpHeaders prepareHeaders(TbMsgMetaData metaData) { + private HttpHeaders prepareHeaders(TbMsg msg) { HttpHeaders headers = new HttpHeaders(); - config.getHeaders().forEach((k, v) -> headers.add(TbNodeUtils.processPattern(k, metaData), TbNodeUtils.processPattern(v, metaData))); + config.getHeaders().forEach((k, v) -> headers.add(TbNodeUtils.processPattern(k, msg), TbNodeUtils.processPattern(v, msg))); + ClientCredentials credentials = config.getCredentials(); + if (CredentialsType.BASIC == credentials.getType()) { + BasicCredentials basicCredentials = (BasicCredentials) credentials; + String authString = basicCredentials.getUsername() + ":" + basicCredentials.getPassword(); + String encodedAuthString = new String(Base64.encodeBase64(authString.getBytes(StandardCharsets.UTF_8))); + headers.add("Authorization", "Basic " + encodedAuthString); + } return headers; } @@ -259,4 +274,5 @@ public class TbHttpClient { throw new TbNodeException("Proxy port out of range:" + proxyPort); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index 92a8c59b6c..b3eb982287 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -18,6 +18,8 @@ package org.thingsboard.rule.engine.rest; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.credentials.AnonymousCredentials; +import org.thingsboard.rule.engine.credentials.ClientCredentials; import java.util.Collections; import java.util.Map; @@ -42,6 +44,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function h(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function C(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var v,F=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-node-empty-config",template:"
"}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],i}return y(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]]})},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return y(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(v||(v={}));var M,P=new Map([[v.CUSTOMER,"tb.rulenode.originator-customer"],[v.TENANT,"tb.rulenode.originator-tenant"],[v.RELATED,"tb.rulenode.originator-related"],[v.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,w=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,D=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var K,B=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(K||(K={}));var U,j,H,G=new Map([[K.TITLE,"tb.rulenode.entity-details-title"],[K.COUNTRY,"tb.rulenode.entity-details-country"],[K.STATE,"tb.rulenode.entity-details-state"],[K.ZIP,"tb.rulenode.entity-details-zip"],[K.ADDRESS,"tb.rulenode.entity-details-address"],[K.ADDRESS2,"tb.rulenode.entity-details-address2"],[K.PHONE,"tb.rulenode.entity-details-phone"],[K.EMAIL,"tb.rulenode.entity-details-email"],[K.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(U||(U={})),function(e){e.ASC="ASC",e.DESC="DESC"}(j||(j={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(H||(H={}));var z,$=new Map([[H.STANDARD,"tb.rulenode.sqs-queue-standard"],[H.FIFO,"tb.rulenode.sqs-queue-fifo"]]),Q=["anonymous","basic","cert.PEM"],_=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),W=["sas","cert.PEM"],J=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(z||(z={}));var Y=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],Z=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]),X=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return y(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=C(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",String)],r.prototype,"requiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"keyRequiredText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valText",void 0),b([t.Input(),h("design:type",String)],r.prototype,"valRequiredText",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=b([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText }}\n {{ valText }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),h("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-device-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return y(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=b([t.Component({selector:"tb-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r,n,o){var i,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.truncate=n,s.fb=o,s.placeholder="tb.rulenode.message-type",s.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],s.messageTypes=[],s.messageTypesList=[],s.searchText="",s.propagateChange=function(e){},s.messageTypeConfigForm=s.fb.group({messageType:[null]});try{for(var u=C(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;s.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return s}var l;return y(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},b([t.Input(),h("design:type",Boolean),h("design:paramtypes",[Boolean])],r.prototype,"required",null),b([t.Input(),h("design:type",String)],r.prototype,"label",void 0),b([t.Input(),h("design:type",Object)],r.prototype,"placeholder",void 0),b([t.Input(),h("design:type",Boolean)],r.prototype,"disabled",void 0),b([t.ViewChild("chipList",{static:!1}),h("design:type",d.MatChipList)],r.prototype,"chipList",void 0),b([t.ViewChild("messageTypeAutocomplete",{static:!1}),h("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),b([t.ViewChild("messageTypeInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=b([t.Component({selector:"tb-message-types-config",template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),h("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),le=function(){function e(){}return e=b([t.NgModule({declarations:[ne,ae,oe,ie],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule],exports:[ne,ae,oe,ie]})],e)}(),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=H,n.sqsQueueTypes=Object.keys(H),n.sqsQueueTypeTranslationsMap=$,n}return y(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Y,n.ToByteStandartCharsetTypeTranslationMap=Z,n}return y(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allMqttCredentialsTypes=Q,n.mqttCredentialsTypeTranslationsMap=_,n}return y(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],username:[e&&e.credentials?e.credentials.username:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;switch(t){case"anonymous":e.credentials={type:t};break;case"basic":e.credentials={type:t,username:e.credentials.username,password:e.credentials.password};break;case"cert.PEM":delete e.credentials.username}return e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.mqttConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("username").setValidators([]),t.get("password").setValidators([]),t.get("caCert").setValidators([]),t.get("caCertFileName").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"anonymous":break;case"basic":t.get("username").setValidators([i.Validators.required]),t.get("password").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("caCert").setValidators([i.Validators.required]),t.get("caCertFileName").setValidators([i.Validators.required]),t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("username").updateValueAndValidity({emitEvent:e}),t.get("password").updateValueAndValidity({emitEvent:e}),t.get("caCert").updateValueAndValidity({emitEvent:e}),t.get("caCertFileName").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n \n tb.rulenode.credentials\n \n {{ mqttCredentialsTypeTranslationsMap.get(mqttConfigForm.get(\'credentials\').get(\'type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ mqttCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return y(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(z),n}return y(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[i.Validators.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n \n \n \n tb.rulenode.max-parallel-requests-count\n \n \n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return y(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return y(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=W,n.azureIotHubCredentialsTypeTranslationsMap=J,n}return y(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.sendSmsConfigForm},r.prototype.onConfigurationSet=function(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[i.Validators.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[i.Validators.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmsSettings"]},r.prototype.updateValidators=function(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([i.Validators.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-action-node-send-sms-config",template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Fe=function(){function e(){}return e=b([t.NgModule({declarations:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he,Ce,ve],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule,le],exports:[x,T,q,S,I,k,N,V,E,A,L,X,ee,te,re,se,me,ue,de,pe,ce,fe,ge,ye,be,he,Ce,ve]})],e)}(),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return y(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=w,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=B,n}return y(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return y(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ke=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ve=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=C(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("alarmStatusInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=b([t.Component({selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ee=function(){function e(){}return e=b([t.NgModule({declarations:[xe,Te,qe,Se,Ie,ke,Ne,Ve],imports:[r.CommonModule,a.SharedModule,le],exports:[xe,Te,qe,Se,Ie,ke,Ne,Ve]})],e)}(),Ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=G,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=C(Object.keys(K)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(K[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return y(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(G.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(G.get(K[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},b([t.ViewChild("detailsInput",{static:!1}),h("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=b([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),h("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return y(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n.fetchMode=U,n.fetchModes=Object.keys(U),n.samplingOrders=Object.keys(j),n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=D,n}return y(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===U.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ke=function(){function e(){}return e=b([t.NgModule({declarations:[Ae,Le,Me,Pe,Re,we,Oe,De],imports:[r.CommonModule,a.SharedModule,le],exports:[Ae,Le,Me,Pe,Re,we,Oe,De]})],e)}(),Be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=v,n.originatorSources=Object.keys(v),n.originatorSourceTranslationMap=P,n}return y(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===v.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ue=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return y(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},b([t.ViewChild("jsFuncComponent",{static:!0}),h("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=b([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),je=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return y(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=b([t.Component({selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}),h("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),He=function(){function e(){}return e=b([t.NgModule({declarations:[Be,Ue,je],imports:[r.CommonModule,a.SharedModule,le],exports:[Be,Ue,je]})],e)}(),Ge=function(){function e(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-name-pattern-hint":"Name pattern, use ${metaKeyName} to substitute variables from metadata","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-type-pattern-hint":"Type pattern, use ${metaKeyName} to substitute variables from metadata","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-name-pattern-hint":"Customer name pattern, use ${metaKeyName} to substitute variables from metadata","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","client-attributes-hint":"Client attributes, use ${metaKeyName} to substitute variables from metadata","shared-attributes":"Shared attributes","shared-attributes-hint":"Shared attributes, use ${metaKeyName} to substitute variables from metadata","server-attributes":"Server attributes","server-attributes-hint":"Server attributes, use ${metaKeyName} to substitute variables from metadata","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","latest-timeseries-hint":"Latest timeseries, use ${metaKeyName} to substitute variables from metadata","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use metadata period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds metadata pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","period-in-seconds-pattern-hint":"Period in seconds pattern, use ${metaKeyName} to substitute variables from metadata","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","from-template-hint":"From address template, use ${metaKeyName} to substitute variables from metadata","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":"Comma separated address list, use ${metaKeyName} to substitute variables from metadata","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","subject-template-hint":"Mail subject template, use ${metaKeyName} to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use ${metaKeyName} to substitute variables from metadata","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","endpoint-url-pattern-hint":"HTTP URL address pattern, use ${metaKeyName} to substitute variables from metadata","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":"Use ${metaKeyName} in header/value fields to substitute variables from metadata",header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required","mqtt-topic-pattern-hint":"MQTT topic pattern, use ${metaKeyName} to substitute variables from metadata",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","topic-arn-pattern-hint":"Topic ARN pattern, use ${metaKeyName} to substitute variables from metadata","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","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","queue-url-pattern-hint":"Queue URL pattern, use ${metaKeyName} to substitute variables from metadata","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":"Use ${metaKeyName} in name/value fields to substitute variables from metadata","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","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","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.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use metadata interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","start-interval-pattern-hint":"Start interval pattern, use ${metaKeyName} to substitute variables from metadata","end-interval-pattern-hint":"End interval pattern, use ${metaKeyName} to substitute variables from metadata","smtp-protocol":"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-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","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","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":"Comma separated Phone Numbers, use ${metaKeyName} to substitute variables from metadata","sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","sms-message-template-hint":"SMS message template, use ${metaKeyName} to substitute variables from metadata","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","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":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=b([t.NgModule({declarations:[F],imports:[r.CommonModule,a.SharedModule],exports:[Fe,Ee,Ke,He,F]}),h("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=Ge,e.ɵa=F,e.ɵb=Fe,e.ɵba=be,e.ɵbb=he,e.ɵbc=Ce,e.ɵbd=ve,e.ɵbe=le,e.ɵbf=ne,e.ɵbg=ae,e.ɵbh=oe,e.ɵbi=ie,e.ɵbj=Ee,e.ɵbk=xe,e.ɵbl=Te,e.ɵbm=qe,e.ɵbn=Se,e.ɵbo=Ie,e.ɵbp=ke,e.ɵbq=Ne,e.ɵbr=Ve,e.ɵbs=Ke,e.ɵbt=Ae,e.ɵbu=Le,e.ɵbv=Me,e.ɵbw=Pe,e.ɵbx=Re,e.ɵby=we,e.ɵbz=Oe,e.ɵc=x,e.ɵca=De,e.ɵcb=He,e.ɵcc=Be,e.ɵcd=Ue,e.ɵce=je,e.ɵd=T,e.ɵe=q,e.ɵf=S,e.ɵg=I,e.ɵh=k,e.ɵi=N,e.ɵj=V,e.ɵk=E,e.ɵl=A,e.ɵm=L,e.ɵn=X,e.ɵo=ee,e.ɵp=te,e.ɵq=re,e.ɵr=se,e.ɵs=me,e.ɵt=ue,e.ɵu=de,e.ɵv=pe,e.ɵw=ce,e.ɵx=fe,e.ɵy=ge,e.ɵz=ye,Object.defineProperty(e,"__esModule",{value:!0})})); + ***************************************************************************** */var y=function(e,t){return(y=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function b(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}y(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function h(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function C(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function v(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var F,x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-node-empty-config",template:"
"}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-attributes-config",template:'
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.notify-device\' | translate }}\n \n
tb.rulenode.notify-device-hint
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-timeseries-config",template:'
\n \n tb.rulenode.default-ttl\n \n \n {{ \'tb.rulenode.default-ttl-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-default-ttl-message\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-request-config",template:'
\n \n tb.rulenode.timeout-sec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-message\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.create-customer-if-not-exists\' | translate }}\n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\n \n \n \n
\n \n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],i}return b(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]]})},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n
\n
\n \n tb.rulenode.alarm-type\n \n \n {{ \'tb.rulenode.alarm-type-required\' | translate }}\n \n \n \n \n tb.rulenode.alarm-severity\n \n \n {{ alarmSeverityTranslationMap.get(severity) | translate }}\n \n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.propagate\' | translate }}\n \n
\n \n tb.rulenode.relation-types-list\n \n \n {{key}}\n close\n \n \n \n tb.rulenode.relation-types-list-hint\n \n
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-create-relation-config",template:'
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-type-pattern\n \n \n {{ \'tb.rulenode.entity-type-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n
\n \n {{ \'tb.rulenode.create-entity-if-not-exists\' | translate }}\n \n
tb.rulenode.create-entity-if-not-exists-hint
\n
\n \n {{ \'tb.rulenode.remove-current-relations\' | translate }}\n \n
tb.rulenode.remove-current-relations-hint
\n \n {{ \'tb.rulenode.change-originator-to-related-entity\' | translate }}\n \n
tb.rulenode.change-originator-to-related-entity-hint
\n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(e){this.msgDelayConfigForm=this.fb.group({useMetadataPeriodInSecondsPatterns:[!!e&&e.useMetadataPeriodInSecondsPatterns,[]],periodInSeconds:[e?e.periodInSeconds:null,[]],periodInSecondsPattern:[e?e.periodInSecondsPattern:null,[]],maxPendingMsgs:[e?e.maxPendingMsgs:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-msg-delay-config",template:'
\n \n {{ \'tb.rulenode.use-metadata-period-in-seconds-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-period-in-seconds-patterns-hint
\n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-0-seconds-message\' | translate }}\n \n \n \n \n tb.rulenode.period-in-seconds-pattern\n \n \n {{ \'tb.rulenode.period-in-seconds-pattern-required\' | translate }}\n \n \n \n \n \n tb.rulenode.max-pending-messages\n \n \n {{ \'tb.rulenode.max-pending-messages-required\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n {{ \'tb.rulenode.max-pending-messages-range\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-delete-relation-config",template:'
\n \n {{ \'tb.rulenode.delete-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.delete-relation-hint
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.entity-name-pattern\n \n \n {{ \'tb.rulenode.entity-name-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.relation-type-pattern\n \n \n {{ \'tb.rulenode.relation-type-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.entity-cache-expiration\n \n \n {{ \'tb.rulenode.entity-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.entity-cache-expiration-range\' | translate }}\n \n tb.rulenode.entity-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),P=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-generator-config",template:'
\n \n tb.rulenode.message-count\n \n \n {{ \'tb.rulenode.message-count-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-message-count-message\' | translate }}\n \n \n \n tb.rulenode.period-seconds\n \n \n {{ \'tb.rulenode.period-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-period-seconds-message\' | translate }}\n \n \n
\n \n \n \n
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(F||(F={}));var M,w=new Map([[F.CUSTOMER,"tb.rulenode.originator-customer"],[F.TENANT,"tb.rulenode.originator-tenant"],[F.RELATED,"tb.rulenode.originator-related"],[F.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,D=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,K=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var B,G=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(B||(B={}));var H,U,j,z=new Map([[B.TITLE,"tb.rulenode.entity-details-title"],[B.COUNTRY,"tb.rulenode.entity-details-country"],[B.STATE,"tb.rulenode.entity-details-state"],[B.ZIP,"tb.rulenode.entity-details-zip"],[B.ADDRESS,"tb.rulenode.entity-details-address"],[B.ADDRESS2,"tb.rulenode.entity-details-address2"],[B.PHONE,"tb.rulenode.entity-details-phone"],[B.EMAIL,"tb.rulenode.entity-details-email"],[B.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(H||(H={})),function(e){e.ASC="ASC",e.DESC="DESC"}(U||(U={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(j||(j={}));var Q,_=new Map([[j.STANDARD,"tb.rulenode.sqs-queue-standard"],[j.FIFO,"tb.rulenode.sqs-queue-fifo"]]),$=["anonymous","basic","cert.PEM"],W=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),J=["sas","cert.PEM"],Y=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(Q||(Q={}));var Z=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],X=new Map([["US-ASCII","tb.rulenode.charset-us-ascii"],["ISO-8859-1","tb.rulenode.charset-iso-8859-1"],["UTF-8","tb.rulenode.charset-utf-8"],["UTF-16BE","tb.rulenode.charset-utf-16be"],["UTF-16LE","tb.rulenode.charset-utf-16le"],["UTF-16","tb.rulenode.charset-utf-16"]]),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=G,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]],minInsideDuration:[e?e.minInsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoActionConfigForm.get("centerLatitude").setValidators([]),this.geoActionConfigForm.get("centerLongitude").setValidators([]),this.geoActionConfigForm.get("range").setValidators([]),this.geoActionConfigForm.get("rangeUnit").setValidators([])):(this.geoActionConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoActionConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoActionConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.min-inside-duration\n \n \n {{ \'tb.rulenode.min-inside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-inside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.min-outside-duration\n \n \n {{ \'tb.rulenode.min-outside-duration-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.min-outside-duration-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-msg-count-config",template:'
\n \n tb.rulenode.interval-seconds\n \n \n {{ \'tb.rulenode.interval-seconds-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-interval-seconds-message\' | translate }}\n \n \n \n tb.rulenode.output-timeseries-key-prefix\n \n \n {{ \'tb.rulenode.output-timeseries-key-prefix-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.prototype.prepareOutputConfig=function(e){return e.tableName=e.tableName.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-custom-table-config",template:'
\n \n tb.rulenode.custom-table-name\n \n \n {{ \'tb.rulenode.custom-table-name-required\' | translate }}\n \n tb.rulenode.custom-table-hint\n \n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return b(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=v(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",String)],r.prototype,"requiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyRequiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valRequiredText",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=h([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],styles:[":host .tb-kv-map-config{margin-bottom:16px}:host .tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}:host .tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),C("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-device-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-relations-query-config",template:'
\n \n {{ \'alias.last-level-relation\' | translate }}\n \n
\n \n relation.direction\n \n \n {{ directionTypeTranslations.get(type) | translate }}\n \n \n \n \n tb.rulenode.max-relation-level\n \n \n
\n
relation.relation-filters
\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),le=function(e){function r(t,r,n,o){var i,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.truncate=n,s.fb=o,s.placeholder="tb.rulenode.message-type",s.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],s.messageTypes=[],s.messageTypesList=[],s.searchText="",s.propagateChange=function(e){},s.messageTypeConfigForm=s.fb.group({messageType:[null]});try{for(var u=v(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;s.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return s}var l;return b(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",String)],r.prototype,"label",void 0),h([t.Input(),C("design:type",Object)],r.prototype,"placeholder",void 0),h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.ViewChild("chipList",{static:!1}),C("design:type",d.MatChipList)],r.prototype,"chipList",void 0),h([t.ViewChild("messageTypeAutocomplete",{static:!1}),C("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),h([t.ViewChild("messageTypeInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=h([t.Component({selector:"tb-message-types-config",template:'\n {{ label }}\n \n \n {{messageType.name}}\n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-message-types-found\n
\n \n \n {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\n \n \n \n tb.rulenode.create-new-message-type\n \n
\n
\n
\n \n {{ \'tb.rulenode.message-types-required\' | translate }}\n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),C("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.subscriptions=[],n.disableCertPemCredentials=!1,n.allCredentialsTypes=$,n.credentialsTypeTranslationsMap=W,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.credentialsConfigFormGroup=this.fb.group({type:[null,[i.Validators.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.pipe(f.distinctUntilChanged()).subscribe((function(){e.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((function(){e.credentialsTypeChanged()})))},r.prototype.ngOnChanges=function(e){var t,r,n=this;try{for(var a=v(Object.keys(e)),o=a.next();!o.done;o=a.next()){var i=o.value,l=e[i];if(!l.firstChange&&l.currentValue!==l.previousValue)if(l.currentValue&&"disableCertPemCredentials"===i)"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((function(){n.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}catch(e){t={error:e}}finally{try{o&&!o.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}},r.prototype.ngOnDestroy=function(){this.subscriptions.forEach((function(e){return e.unsubscribe()}))},r.prototype.writeValue=function(e){s.isDefinedAndNotNull(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators(!1))},r.prototype.setDisabledState=function(e){e?this.credentialsConfigFormGroup.disable():(this.credentialsConfigFormGroup.enable(),this.updateValidators())},r.prototype.updateView=function(){var e=this.credentialsConfigFormGroup.value,t=e.type;switch(t){case"anonymous":e={type:t};break;case"basic":e={type:t,username:e.username,password:e.password};break;case"cert.PEM":delete e.username}this.propagateChange(e)},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.validate=function(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}},r.prototype.credentialsTypeChanged=function(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()},r.prototype.updateValidators=function(e){void 0===e&&(e=!1);var t=this.credentialsConfigFormGroup.get("type").value;switch(e&&this.credentialsConfigFormGroup.reset({type:t},{emitEvent:!1}),this.credentialsConfigFormGroup.setValidators([]),this.credentialsConfigFormGroup.get("username").setValidators([]),this.credentialsConfigFormGroup.get("password").setValidators([]),t){case"anonymous":break;case"basic":this.credentialsConfigFormGroup.get("username").setValidators([i.Validators.required]),this.credentialsConfigFormGroup.get("password").setValidators([i.Validators.required]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(i.Validators.required,[["caCert","caCertFileName"],["privateKey","privateKeyFileName","cert","certFileName"]])])}this.credentialsConfigFormGroup.get("username").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.get("password").updateValueAndValidity({emitEvent:e}),this.credentialsConfigFormGroup.updateValueAndValidity({emitEvent:e})},r.prototype.requiredFilesSelected=function(e,t){return void 0===t&&(t=null),function(r){return t||(t=[Object.keys(r.controls)]),(null==r?void 0:r.controls)&&t.some((function(t){return t.every((function(t){return!e(r.controls[t])}))}))?null:{notAllRequiredFilesSelected:!0}}},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",Object)],r.prototype,"disableCertPemCredentials",void 0),r=n=h([t.Component({selector:"tb-credentials-config",template:'
\n \n \n tb.rulenode.credentials\n \n {{ credentialsTypeTranslationsMap.get(credentialsConfigFormGroup.get(\'type\').value) | translate }}\n \n \n \n \n tb.rulenode.credentials-type\n \n \n {{ credentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.username\n \n \n {{ \'tb.rulenode.username-required\' | translate }}\n \n \n \n tb.rulenode.password\n \n \n {{ \'tb.rulenode.password-required\' | translate }}\n \n \n \n \n
{{ \'tb.rulenode.credentials-pem-hint\' | translate }}
\n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n
\n
\n
\n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),me=function(){function e(e){this.sanitizer=e}return e.prototype.transform=function(e){return this.sanitizer.bypassSecurityTrustHtml(e)},e.ctorParameters=function(){return[{type:g.DomSanitizer}]},e=h([t.Pipe({name:"safeHtml"}),C("design:paramtypes",[g.DomSanitizer])],e)}(),ue=function(){function e(){}return e=h([t.NgModule({declarations:[ae,oe,ie,le,se,me],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule],exports:[ae,oe,ie,le,se,me]})],e)}(),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-un-assign-to-customer-config",template:'
\n \n tb.rulenode.customer-name-pattern\n \n \n {{ \'tb.rulenode.customer-name-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.customer-cache-expiration\n \n \n {{ \'tb.rulenode.customer-cache-expiration-required\' | translate }}\n \n \n {{ \'tb.rulenode.customer-cache-expiration-range\' | translate }}\n \n tb.rulenode.customer-cache-expiration-hint\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-sns-config",template:'
\n \n tb.rulenode.topic-arn-pattern\n \n \n {{ \'tb.rulenode.topic-arn-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=j,n.sqsQueueTypes=Object.keys(j),n.sqsQueueTypeTranslationsMap=_,n}return b(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-sqs-config",template:'
\n \n tb.rulenode.queue-type\n \n \n {{ sqsQueueTypeTranslationsMap.get(type) | translate }}\n \n \n \n \n tb.rulenode.queue-url-pattern\n \n \n {{ \'tb.rulenode.queue-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.delay-seconds\n \n \n {{ \'tb.rulenode.min-delay-seconds-message\' | translate }}\n \n \n {{ \'tb.rulenode.max-delay-seconds-message\' | translate }}\n \n \n \n
\n \n \n \n tb.rulenode.aws-access-key-id\n \n \n {{ \'tb.rulenode.aws-access-key-id-required\' | translate }}\n \n \n \n tb.rulenode.aws-secret-access-key\n \n \n {{ \'tb.rulenode.aws-secret-access-key-required\' | translate }}\n \n \n \n tb.rulenode.aws-region\n \n \n {{ \'tb.rulenode.aws-region-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-pub-sub-config",template:'
\n \n tb.rulenode.gcp-project-id\n \n \n {{ \'tb.rulenode.gcp-project-id-required\' | translate }}\n \n \n \n tb.rulenode.pubsub-topic-name\n \n \n {{ \'tb.rulenode.pubsub-topic-name-required\' | translate }}\n \n \n \n \n \n
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Z,n.ToByteStandartCharsetTypeTranslationMap=X,n}return b(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-kafka-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.bootstrap-servers\n \n \n {{ \'tb.rulenode.bootstrap-servers-required\' | translate }}\n \n \n \n tb.rulenode.retries\n \n \n {{ \'tb.rulenode.min-retries-message\' | translate }}\n \n \n \n tb.rulenode.batch-size-bytes\n \n \n {{ \'tb.rulenode.min-batch-size-bytes-message\' | translate }}\n \n \n \n tb.rulenode.linger-ms\n \n \n {{ \'tb.rulenode.min-linger-ms-message\' | translate }}\n \n \n \n tb.rulenode.buffer-memory-bytes\n \n \n {{ \'tb.rulenode.min-buffer-memory-bytes-message\' | translate }}\n \n \n \n tb.rulenode.acks\n \n \n {{ ackValue }}\n \n \n \n \n tb.rulenode.key-serializer\n \n \n {{ \'tb.rulenode.key-serializer-required\' | translate }}\n \n \n \n tb.rulenode.value-serializer\n \n \n {{ \'tb.rulenode.value-serializer-required\' | translate }}\n \n \n \n \n \n \n {{ \'tb.rulenode.add-metadata-key-values-as-kafka-headers\' | translate }}\n \n
tb.rulenode.add-metadata-key-values-as-kafka-headers-hint
\n \n tb.rulenode.charset-encoding\n \n \n {{ ToByteStandartCharsetTypeTranslationMap.get(charset) | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-mqtt-config",template:'
\n \n tb.rulenode.topic-pattern\n \n \n {{ \'tb.rulenode.topic-pattern-required\' | translate }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n \n tb.rulenode.connect-timeout\n \n \n {{ \'tb.rulenode.connect-timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n {{ \'tb.rulenode.connect-timeout-range\' | translate }}\n \n \n
\n \n tb.rulenode.client-id\n \n \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return b(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(e){this.rabbitMqConfigForm=this.fb.group({exchangeNamePattern:[e?e.exchangeNamePattern:null,[]],routingKeyPattern:[e?e.routingKeyPattern:null,[]],messageProperties:[e?e.messageProperties:null,[]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],virtualHost:[e?e.virtualHost:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]],automaticRecoveryEnabled:[!!e&&e.automaticRecoveryEnabled,[]],connectionTimeout:[e?e.connectionTimeout:null,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rabbit-mq-config",template:'
\n \n tb.rulenode.exchange-name-pattern\n \n \n \n tb.rulenode.routing-key-pattern\n \n \n \n tb.rulenode.message-properties\n \n \n {{ property }}\n \n \n \n
\n \n tb.rulenode.host\n \n \n {{ \'tb.rulenode.host-required\' | translate }}\n \n \n \n tb.rulenode.port\n \n \n {{ \'tb.rulenode.port-required\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n {{ \'tb.rulenode.port-range\' | translate }}\n \n \n
\n \n tb.rulenode.virtual-host\n \n \n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n \n {{ \'tb.rulenode.automatic-recovery\' | translate }}\n \n \n tb.rulenode.connection-timeout-ms\n \n \n {{ \'tb.rulenode.min-connection-timeout-ms-message\' | translate }}\n \n \n \n tb.rulenode.handshake-timeout-ms\n \n \n {{ \'tb.rulenode.min-handshake-timeout-ms-message\' | translate }}\n \n \n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(Q),n}return b(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],enableProxy:[!!e&&e.enableProxy,[]],useSystemProxyProperties:[!!e&&e.enableProxy,[]],proxyScheme:[e?e.proxyHost:null,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],readTimeoutMs:[e?e.readTimeoutMs:null,[]],maxParallelRequestsCount:[e?e.maxParallelRequestsCount:null,[i.Validators.min(0)]],headers:[e?e.headers:null,[]],useRedisQueueForMsgPersistence:[!!e&&e.useRedisQueueForMsgPersistence,[]],trimQueue:[!!e&&e.trimQueue,[]],maxQueueSize:[e?e.maxQueueSize:null,[]],credentials:[e?e.credentials:null,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.min(0)]):this.restApiCallConfigForm.get("maxQueueSize").setValidators([]),this.restApiCallConfigForm.get("readTimeoutMs").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("maxQueueSize").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e}),this.restApiCallConfigForm.get("credentials").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rest-api-call-config",template:'
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ \'tb.rulenode.endpoint-url-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n \n {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }}\n \n
\n \n {{ \'tb.rulenode.use-system-proxy-properties\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ \'tb.rulenode.use-redis-queue\' | translate }}\n \n
\n \n {{ \'tb.rulenode.trim-redis-queue\' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return b(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.sendEmailConfigForm=this.fb.group({useSystemSmtpSettings:[!!e&&e.useSystemSmtpSettings,[]],smtpProtocol:[e?e.smtpProtocol:null,[]],smtpHost:[e?e.smtpHost:null,[]],smtpPort:[e?e.smtpPort:null,[]],timeout:[e?e.timeout:null,[]],enableTls:[!!e&&e.enableTls,[]],tlsVersion:[e?e.tlsVersion:null,[]],enableProxy:[!!e&&e.enableProxy,[]],proxyHost:[e?e.proxyHost:null,[]],proxyPort:[e?e.proxyPort:null,[]],proxyUser:[e?e.proxyUser:null,[]],proxyPassword:[e?e.proxyPassword:null,[]],username:[e?e.username:null,[]],password:[e?e.password:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=this.sendEmailConfigForm.get("enableProxy").value;t?(this.sendEmailConfigForm.get("smtpProtocol").setValidators([]),this.sendEmailConfigForm.get("smtpHost").setValidators([]),this.sendEmailConfigForm.get("smtpPort").setValidators([]),this.sendEmailConfigForm.get("timeout").setValidators([]),this.sendEmailConfigForm.get("proxyHost").setValidators([]),this.sendEmailConfigForm.get("proxyPort").setValidators([])):(this.sendEmailConfigForm.get("smtpProtocol").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])),this.sendEmailConfigForm.get("smtpProtocol").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("smtpPort").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("timeout").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyHost").updateValueAndValidity({emitEvent:e}),this.sendEmailConfigForm.get("proxyPort").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-send-email-config",template:'
\n \n {{ \'tb.rulenode.use-system-smtp-settings\' | translate }}\n \n
\n \n tb.rulenode.smtp-protocol\n \n \n {{ smtpProtocol.toUpperCase() }}\n \n \n \n
\n \n tb.rulenode.smtp-host\n \n \n {{ \'tb.rulenode.smtp-host-required\' | translate }}\n \n \n \n tb.rulenode.smtp-port\n \n \n {{ \'tb.rulenode.smtp-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n {{ \'tb.rulenode.smtp-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.timeout-msec\n \n \n {{ \'tb.rulenode.timeout-required\' | translate }}\n \n \n {{ \'tb.rulenode.min-timeout-msec-message\' | translate }}\n \n \n \n {{ \'tb.rulenode.enable-tls\' | translate }}\n \n \n tb.rulenode.tls-version\n \n \n {{ tlsVersion }}\n \n \n \n \n {{ \'tb.rulenode.enable-proxy\' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-host\n \n \n {{ \'tb.rulenode.proxy-host-required\' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ \'tb.rulenode.proxy-port-required\' | translate }}\n \n \n {{ \'tb.rulenode.proxy-port-range\' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n \n tb.rulenode.username\n \n \n \n tb.rulenode.password\n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return b(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=J,n.azureIotHubCredentialsTypeTranslationsMap=Y,n}return b(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.required]],sasKey:[e&&e.credentials?e.credentials.sasKey:null,[]],caCert:[e&&e.credentials?e.credentials.caCert:null,[]],caCertFileName:[e&&e.credentials?e.credentials.caCertFileName:null,[]],privateKey:[e&&e.credentials?e.credentials.privateKey:null,[]],privateKeyFileName:[e&&e.credentials?e.credentials.privateKeyFileName:null,[]],cert:[e&&e.credentials?e.credentials.cert:null,[]],certFileName:[e&&e.credentials?e.credentials.certFileName:null,[]],password:[e&&e.credentials?e.credentials.password:null,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.required])}t.get("sasKey").updateValueAndValidity({emitEvent:e}),t.get("privateKey").updateValueAndValidity({emitEvent:e}),t.get("privateKeyFileName").updateValueAndValidity({emitEvent:e}),t.get("cert").updateValueAndValidity({emitEvent:e}),t.get("certFileName").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-azure-iot-hub-config",template:'
\n \n tb.rulenode.topic\n \n \n {{ \'tb.rulenode.topic-required\' | translate }}\n \n \n \n tb.rulenode.hostname\n \n \n {{ \'tb.rulenode.hostname-required\' | translate }}\n \n \n \n tb.rulenode.device-id\n \n \n {{ \'tb.rulenode.device-id-required\' | translate }}\n \n \n \n \n \n tb.rulenode.credentials\n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(azureIotHubConfigForm.get(\'credentials.type\').value) | translate }}\n \n \n
\n \n tb.rulenode.credentials-type\n \n \n {{ azureIotHubCredentialsTypeTranslationsMap.get(credentialsType) | translate }}\n \n \n \n {{ \'tb.rulenode.credentials-type-required\' | translate }}\n \n \n
\n \n \n \n \n tb.rulenode.sas-key\n \n \n {{ \'tb.rulenode.sas-key-required\' | translate }}\n \n \n \n \n \n \n \n \n \n \n \n \n \n tb.rulenode.private-key-password\n \n \n \n
\n
\n
\n
\n
\n',styles:[":host .tb-mqtt-credentials-panel-group{margin:0 6px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-device-profile-config",template:'
\n \n {{ \'tb.rulenode.persist-alarm-rules\' | translate }}\n \n \n {{ \'tb.rulenode.fetch-alarm-rules\' | translate }}\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.sendSmsConfigForm},r.prototype.onConfigurationSet=function(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[i.Validators.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[i.Validators.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmsSettings"]},r.prototype.updateValidators=function(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([i.Validators.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-send-sms-config",template:'
\n \n tb.rulenode.numbers-to-template\n \n \n {{ \'tb.rulenode.numbers-to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.sms-message-template\n \n \n {{ \'tb.rulenode.sms-message-template-required\' | translate }}\n \n \n \n \n {{ \'tb.rulenode.use-system-sms-settings\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(){function e(){}return e=h([t.NgModule({declarations:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule,ue],exports:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te]})],e)}(),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n {{ \'tb.rulenode.check-all-keys\' | translate }}\n \n
tb.rulenode.check-all-keys-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-relation-config",template:'
\n \n {{ \'tb.rulenode.check-relation-to-specific-entity\' | translate }}\n \n
tb.rulenode.check-relation-hint
\n \n relation.direction\n \n \n {{ entitySearchDirectionTranslationsMap.get(direction) | translate }}\n \n \n \n
\n \n \n \n \n
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=G,n}return b(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType:null,[]],centerLatitude:[e?e.centerLatitude:null,[]],centerLongitude:[e?e.centerLatitude:null,[]],range:[e?e.range:null,[]],rangeUnit:[e?e.rangeUnit:null,[]],polygonsDefinition:[e?e.polygonsDefinition:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.CIRCLE?(this.geoFilterConfigForm.get("centerLatitude").setValidators([]),this.geoFilterConfigForm.get("centerLongitude").setValidators([]),this.geoFilterConfigForm.get("range").setValidators([]),this.geoFilterConfigForm.get("rangeUnit").setValidators([])):(this.geoFilterConfigForm.get("centerLatitude").setValidators([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),this.geoFilterConfigForm.get("centerLatitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("centerLongitude").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("range").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("rangeUnit").updateValueAndValidity({emitEvent:e}),this.geoFilterConfigForm.get("polygonsDefinition").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-gps-geofencing-config",template:'
\n \n tb.rulenode.latitude-key-name\n \n \n {{ \'tb.rulenode.latitude-key-name-required\' | translate }}\n \n \n \n tb.rulenode.longitude-key-name\n \n \n {{ \'tb.rulenode.longitude-key-name-required\' | translate }}\n \n \n \n {{ \'tb.rulenode.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n \n tb.rulenode.circle-center-latitude\n \n \n {{ \'tb.rulenode.circle-center-latitude-required\' | translate }}\n \n \n \n tb.rulenode.circle-center-longitude\n \n \n {{ \'tb.rulenode.circle-center-longitude-required\' | translate }}\n \n \n
\n
\n \n tb.rulenode.range\n \n \n {{ \'tb.rulenode.range-required\' | translate }}\n \n \n \n tb.rulenode.range-units\n \n \n {{ rangeUnitTranslationMap.get(type) | translate }}\n \n \n \n
\n
\n
\n
\n \n tb.rulenode.polygon-definition\n \n \n {{ \'tb.rulenode.polygon-definition-required\' | translate }}\n \n \n
\n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return b(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ee=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=v(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("alarmStatusInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=h([t.Component({selector:"tb-filter-node-check-alarm-status-config",template:'
\n \n tb.rulenode.alarm-status-filter\n \n \n \n {{alarmStatusTranslationsMap.get(alarmStatus) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-alarm-status-matching\n
\n
\n
\n
\n
\n \n
\n\n\n\n'}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Pe=function(){function e(){}return e=h([t.NgModule({declarations:[Se,Ie,ke,Ne,Ve,Ee,Ae,Le],imports:[r.CommonModule,a.SharedModule,ue],exports:[Se,Ie,ke,Ne,Ve,Ee,Ae,Le]})],e)}(),Me=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=z,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=v(Object.keys(B)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(B[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(z.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(z.get(B[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("detailsInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=h([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
tb.rulenode.get-latest-value-with-ts-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
tb.rulenode.get-latest-value-with-ts-hint
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n.fetchMode=H,n.fetchModes=Object.keys(H),n.samplingOrders=Object.keys(U),n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===H.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\n \n \n {{ \'tb.rulenode.start-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\n \n \n {{ \'tb.rulenode.end-interval-value-required\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n {{ \'tb.rulenode.time-value-range\' | translate }}\n \n \n \n tb.rulenode.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),He=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.calculateDeltaConfigForm},r.prototype.onConfigurationSet=function(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e?e.inputValueKey:null,[i.Validators.required]],outputValueKey:[e?e.outputValueKey:null,[i.Validators.required]],useCache:[e?e.useCache:null,[]],addPeriodBetweenMsgs:[!!e&&e.addPeriodBetweenMsgs,[]],periodValueKey:[e?e.periodValueKey:null,[]],round:[e?e.round:null,[i.Validators.min(0),i.Validators.max(15)]],tellFailureIfDeltaIsNegative:[e?e.tellFailureIfDeltaIsNegative:null,[]]})},r.prototype.updateValidators=function(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([i.Validators.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})},r.prototype.validatorTriggers=function(){return["addPeriodBetweenMsgs"]},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-calculate-delta-config",template:'
\n
\n \n tb.rulenode.input-value-key\n \n \n {{ \'tb.rulenode.input-value-key-required\' | translate }}\n \n \n \n tb.rulenode.output-value-key\n \n \n {{ \'tb.rulenode.output-value-key-required\' | translate }}\n \n \n \n tb.rulenode.round\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.use-cache\' | translate }}\n \n \n {{ \'tb.rulenode.tell-failure-if-delta-is-negative\' | translate }}\n \n \n {{ \'tb.rulenode.add-period-between-msgs\' | translate }}\n \n \n tb.rulenode.period-value-key\n \n \n {{ \'tb.rulenode.period-value-key-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ue=function(){function e(){}return e=h([t.NgModule({declarations:[Me,we,Re,De,Oe,Ke,Be,Ge,He],imports:[r.CommonModule,a.SharedModule,ue],exports:[Me,we,Re,De,Oe,Ke,Be,Ge,He]})],e)}(),je=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=F,n.originatorSources=Object.keys(F),n.originatorSourceTranslationMap=w,n}return b(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===F.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-transformation-node-change-originator-config",template:'
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ze=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-transformation-node-to-email-config",template:'
\n \n tb.rulenode.from-template\n \n \n {{ \'tb.rulenode.from-template-required\' | translate }}\n \n \n \n \n tb.rulenode.to-template\n \n \n {{ \'tb.rulenode.to-template-required\' | translate }}\n \n \n \n \n tb.rulenode.cc-template\n \n \n \n \n tb.rulenode.bcc-template\n \n \n \n \n tb.rulenode.subject-template\n \n \n {{ \'tb.rulenode.subject-template-required\' | translate }}\n \n \n \n \n tb.rulenode.body-template\n \n \n {{ \'tb.rulenode.body-template-required\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),_e=function(){function e(){}return e=h([t.NgModule({declarations:[je,ze,Qe],imports:[r.CommonModule,a.SharedModule,ue],exports:[je,ze,Qe]})],e)}(),$e=function(){function e(e){!function(e){e.setTranslation("en_US",{tb:{rulenode:{"create-entity-if-not-exists":"Create new entity if not exists","create-entity-if-not-exists-hint":"Create a new entity set above if it does not exist.","entity-name-pattern":"Name pattern","entity-name-pattern-required":"Name pattern is required","entity-type-pattern":"Type pattern","entity-type-pattern-required":"Type pattern is required","entity-cache-expiration":"Entities cache expiration time (sec)","entity-cache-expiration-hint":"Specifies maximum time interval allowed to store found entity records. 0 value means that records will never expire.","entity-cache-expiration-required":"Entities cache expiration time is required.","entity-cache-expiration-range":"Entities cache expiration time should be greater than or equal to 0.","customer-name-pattern":"Customer name pattern","customer-name-pattern-required":"Customer name pattern is required","create-customer-if-not-exists":"Create new customer if not exists","customer-cache-expiration":"Customers cache expiration time (sec)","customer-cache-expiration-hint":"Specifies maximum time interval allowed to store found customer records. 0 value means that records will never expire.","customer-cache-expiration-required":"Customers cache expiration time is required.","customer-cache-expiration-range":"Customers cache expiration time should be greater than or equal to 0.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","time-unit-milliseconds":"Milliseconds","time-unit-seconds":"Seconds","time-unit-minutes":"Minutes","time-unit-hours":"Hours","time-unit-days":"Days","time-value-range":"Time value should be in a range from 1 to 2147483647.","start-interval-value-required":"Start interval value is required.","end-interval-value-required":"End interval value is required.",filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","relation-type-pattern":"Relation type pattern","relation-type-pattern-required":"Relation type pattern is required","relation-types-list":"Relation types to propagate","relation-types-list-hint":"If Propagate relation types are not selected, alarms will be propagated without filtering by relation type.","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","source-field":"Source field","source-field-required":"Source field is required.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","originator-alarm-originator":"Alarm Originator","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","use-metadata-period-in-seconds-patterns":"Use period in seconds pattern","use-metadata-period-in-seconds-patterns-hint":"If selected, rule node use period in seconds interval pattern from message metadata or data assuming that intervals are in the seconds.","period-in-seconds-pattern":"Period in seconds pattern","period-in-seconds-pattern-required":"Period in seconds pattern is required","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required","alarm-status-filter":"Alarm status filter","alarm-status-list-empty":"Alarm status list is empty","no-alarm-status-matching":"No alarm status matching were found.",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","mail-address-list-template-hint":'Comma separated address list, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required","request-id-metadata-attribute":"Request Id Metadata attribute name","timeout-sec":"Timeout in seconds","timeout-required":"Timeout is required","min-timeout-message":"Only 0 minimum timeout value is allowed.","endpoint-url-pattern":"Endpoint URL pattern","endpoint-url-pattern-required":"Endpoint URL pattern is required","request-method":"Request method","use-simple-client-http-factory":"Use simple client HTTP factory","read-timeout":"Read timeout in millis","read-timeout-hint":"The value of 0 means an infinite timeout","max-parallel-requests-count":"Max number of parallel requests","max-parallel-requests-count-hint":"The value of 0 specifies no limit in parallel processing",headers:"Headers","headers-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in header/value fields',header:"Header","header-required":"Header is required",value:"Value","value-required":"Value is required","topic-pattern":"Topic pattern","topic-pattern-required":"Topic pattern is required",topic:"Topic","topic-required":"Topic is required","bootstrap-servers":"Bootstrap servers","bootstrap-servers-required":"Bootstrap servers value is required","other-properties":"Other properties",key:"Key","key-required":"Key is required",retries:"Automatically retry times if fails","min-retries-message":"Only 0 minimum retries is allowed.","batch-size-bytes":"Produces batch size in bytes","min-batch-size-bytes-message":"Only 0 minimum batch size is allowed.","linger-ms":"Time to buffer locally (ms)","min-linger-ms-message":"Only 0 ms minimum value is allowed.","buffer-memory-bytes":"Client buffer max size in bytes","min-buffer-memory-message":"Only 0 minimum buffer size is allowed.",acks:"Number of acknowledgments","key-serializer":"Key serializer","key-serializer-required":"Key serializer is required","value-serializer":"Value serializer","value-serializer-required":"Value serializer is required","topic-arn-pattern":"Topic ARN pattern","topic-arn-pattern-required":"Topic ARN pattern is required","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","exchange-name-pattern":"Exchange name pattern","routing-key-pattern":"Routing key pattern","message-properties":"Message properties",host:"Host","host-required":"Host is required",port:"Port","port-required":"Port is required","port-range":"Port should be in a range from 1 to 65535.","virtual-host":"Virtual host",username:"Username",password:"Password","automatic-recovery":"Automatic recovery","connection-timeout-ms":"Connection timeout (ms)","min-connection-timeout-ms-message":"Only 0 ms minimum value is allowed.","handshake-timeout-ms":"Handshake timeout (ms)","min-handshake-timeout-ms-message":"Only 0 ms minimum value is allowed.","client-properties":"Client properties","queue-url-pattern":"Queue URL pattern","queue-url-pattern-required":"Queue URL pattern is required","delay-seconds":"Delay (seconds)","min-delay-seconds-message":"Only 0 seconds minimum value is allowed.","max-delay-seconds-message":"Only 900 seconds maximum value is allowed.",name:"Name","name-required":"Name is required","queue-type":"Queue type","sqs-queue-standard":"Standard","sqs-queue-fifo":"FIFO","gcp-project-id":"GCP project ID","gcp-project-id-required":"GCP project ID is required","gcp-service-account-key":"GCP service account key file","gcp-service-account-key-required":"GCP service account key file is required","pubsub-topic-name":"Topic name","pubsub-topic-name-required":"Topic name is required","message-attributes":"Message attributes","message-attributes-hint":'Use ${metadataKey} for value from metadata, $[messageKey] for value from message body in name/value fields',"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","device-id":"Device ID","device-id-required":"Device ID is required.","clean-session":"Clean session","enable-ssl":"Enable SSL",credentials:"Credentials","credentials-type":"Credentials type","credentials-type-required":"Credentials type is required.","credentials-anonymous":"Anonymous","credentials-basic":"Basic","credentials-pem":"PEM","credentials-pem-hint":"At least Server CA certificate file or a pair of Client certificate and Client private key files are required","credentials-sas":"Shared Access Signature","sas-key":"SAS Key","sas-key-required":"SAS Key is required.",hostname:"Hostname","hostname-required":"Hostname is required.","azure-ca-cert":"CA certificate file","username-required":"Username is required.","password-required":"Password is required.","ca-cert":"Server CA certificate file *","private-key":"Client private key file *",cert:"Client certificate file *","no-file":"No file selected.","drop-file":"Drop a file or click to select a file to upload.","private-key-password":"Private key password","use-system-smtp-settings":"Use system SMTP settings","use-metadata-interval-patterns":"Use interval patterns","use-metadata-interval-patterns-hint":"If selected, rule node use start and end interval patterns from message metadata or data assuming that intervals are in the milliseconds.","use-message-alarm-data":"Use message alarm data","check-all-keys":"Check that all selected keys are present","check-all-keys-hint":"If selected, checks that all specified keys are present in the message data and metadata.","check-relation-to-specific-entity":"Check relation to specific entity","check-relation-hint":"Checks existence of relation to specific entity or to any entity based on direction and relation type.","delete-relation-to-specific-entity":"Delete relation to specific entity","delete-relation-hint":"Deletes relation from the originator of the incoming message to the specified entity or list of entities based on direction and type.","remove-current-relations":"Remove current relations","remove-current-relations-hint":"Removes current relations from the originator of the incoming message based on direction and type.","change-originator-to-related-entity":"Change originator to related entity","change-originator-to-related-entity-hint":"Used to process submitted message as a message from another entity.","start-interval-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern is required","smtp-protocol":"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-range":"SMTP port should be in a range from 1 to 65535.","timeout-msec":"Timeout ms","min-timeout-msec-message":"Only 0 ms minimum value is allowed.","enter-username":"Enter username","enter-password":"Enter password","enable-tls":"Enable TLS","tls-version":"TLS version","enable-proxy":"Enable proxy","use-system-proxy-properties":"Use system proxy properties","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","proxy-scheme":"Proxy scheme","numbers-to-template":"Phone Numbers To Template","numbers-to-template-required":"Phone Numbers To Template is required","numbers-to-template-hint":'Comma separated Phone Numbers, use ${metadataKey} for value from metadata, $[messageKey] for value from message body',"sms-message-template":"SMS message Template","sms-message-template-required":"SMS message Template is required","use-system-sms-settings":"Use system SMS provider settings","min-period-0-seconds-message":"Only 0 second minimum period is allowed.","max-pending-messages":"Maximum pending messages","max-pending-messages-required":"Maximum pending messages is required.","max-pending-messages-range":"Maximum pending messages should be in a range from 1 to 100000.","originator-types-filter":"Originator types filter","interval-seconds":"Interval in seconds","interval-seconds-required":"Interval is required.","min-interval-seconds-message":"Only 1 second minimum interval is allowed.","output-timeseries-key-prefix":"Output timeseries key prefix","output-timeseries-key-prefix-required":"Output timeseries key prefix required.","separator-hint":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","entity-details-zip":"Zip","entity-details-address":"Address","entity-details-address2":"Address2","entity-details-additional_info":"Additional Info","entity-details-phone":"Phone","entity-details-email":"Email","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","entity-details-list-empty":"No entity details selected.","no-entity-details-matching":"No entity details matching were found.","custom-table-name":"Custom table name","custom-table-name-required":"Table Name is required","custom-table-hint":"You should enter the table name without prefix 'cs_tb_'.","message-field":"Message field","message-field-required":"Message field is required.","table-col":"Table column","table-col-required":"Table column is required.","latitude-key-name":"Latitude key name","longitude-key-name":"Longitude key name","latitude-key-name-required":"Latitude key name is required.","longitude-key-name-required":"Longitude key name is required.","fetch-perimeter-info-from-message-metadata":"Fetch perimeter information from message metadata","perimeter-circle":"Circle","perimeter-polygon":"Polygon","perimeter-type":"Perimeter type","circle-center-latitude":"Center latitude","circle-center-latitude-required":"Center latitude is required.","circle-center-longitude":"Center longitude","circle-center-longitude-required":"Center longitude is required.","range-unit-meter":"Meter","range-unit-kilometer":"Kilometer","range-unit-foot":"Foot","range-unit-mile":"Mile","range-unit-nautical-mile":"Nautical mile","range-units":"Range units",range:"Range","range-required":"Range is required.","polygon-definition":"Polygon definition","polygon-definition-required":"Polygon definition is required.","polygon-definition-hint":"Please, use the following format for manual definition of polygon: [[lat1,lon1],[lat2,lon2], ... ,[latN,lonN]].","min-inside-duration":"Minimal inside duration","min-inside-duration-value-required":"Minimal inside duration is required","min-inside-duration-time-unit":"Minimal inside duration time unit","min-outside-duration":"Minimal outside duration","min-outside-duration-value-required":"Minimal outside duration is required","min-outside-duration-time-unit":"Minimal outside duration time unit","tell-failure-if-absent":"Tell Failure","tell-failure-if-absent-hint":'If at least one selected key doesn\'t exist the outbound message will report "Failure".',"get-latest-value-with-ts":"Fetch Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{\\"ts\\":1574329385897,\\"value\\":42}"',"use-redis-queue":"Use redis queue for message persistence","trim-redis-queue":"Trim redis queue","redis-queue-max-size":"Redis queue max size","add-metadata-key-values-as-kafka-headers":"Add Message metadata key-value pairs to Kafka record headers","add-metadata-key-values-as-kafka-headers-hint":"If selected, key-value pairs from message metadata will be added to the outgoing records headers as byte arrays with predefined charset encoding.","charset-encoding":"Charset encoding","charset-encoding-required":"Charset encoding is required.","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":"The queue name can be selected from a drop-down list or add a custom name.","persist-alarm-rules":"Persist state of alarm rules","fetch-alarm-rules":"Fetch state of alarm rules","input-value-key":"Input value key","input-value-key-required":"Input value key is required.","output-value-key":"Output value key","output-value-key-required":"Output value key is required.",round:"Decimals","round-range":"Decimals should be in a range from 0 to 15.","use-cache":"Use cache for latest value","tell-failure-if-delta-is-negative":"Tell Failure if delta is negative","add-period-between-msgs":"Add period between messages","period-value-key":"Period value key","period-key-required":"Period value key is required.","general-pattern-hint":'Hint: use ${metadataKey} for value from metadata, $[messageKey] for value from message body'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=h([t.NgModule({declarations:[x],imports:[r.CommonModule,a.SharedModule],exports:[qe,Pe,Ue,_e,x]}),C("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=$e,e.ɵa=x,e.ɵb=qe,e.ɵba=ve,e.ɵbb=Fe,e.ɵbc=xe,e.ɵbd=Te,e.ɵbe=ue,e.ɵbf=ae,e.ɵbg=oe,e.ɵbh=ie,e.ɵbi=le,e.ɵbj=se,e.ɵbk=me,e.ɵbl=Pe,e.ɵbm=Se,e.ɵbn=Ie,e.ɵbo=ke,e.ɵbp=Ne,e.ɵbq=Ve,e.ɵbr=Ee,e.ɵbs=Ae,e.ɵbt=Le,e.ɵbu=Ue,e.ɵbv=Me,e.ɵbw=we,e.ɵbx=Re,e.ɵby=De,e.ɵbz=Oe,e.ɵc=T,e.ɵca=Ke,e.ɵcb=Be,e.ɵcc=Ge,e.ɵcd=He,e.ɵce=_e,e.ɵcf=je,e.ɵcg=ze,e.ɵch=Qe,e.ɵd=q,e.ɵe=S,e.ɵf=I,e.ɵg=k,e.ɵh=N,e.ɵi=V,e.ɵj=E,e.ɵk=A,e.ɵl=L,e.ɵm=P,e.ɵn=ee,e.ɵo=te,e.ɵp=re,e.ɵq=ne,e.ɵr=de,e.ɵs=pe,e.ɵt=ce,e.ɵu=fe,e.ɵv=ge,e.ɵw=ye,e.ɵx=be,e.ɵy=he,e.ɵz=Ce,Object.defineProperty(e,"__esModule",{value:!0})})); //# sourceMappingURL=rulenode-core-config.umd.min.js.map \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index cc47cbe089..cded6530f5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -27,7 +27,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; @@ -35,7 +35,6 @@ import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; @@ -55,7 +54,8 @@ import java.util.function.Consumer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; @@ -294,7 +294,7 @@ public class TbAlarmNodeTest { when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())) + when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), nullable(JsonNode.class), anyLong())) .thenReturn(Futures.immediateFuture( false)); when(alarmService.findAlarmByIdAsync(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()))).thenReturn(Futures.immediateFuture(activeAlarm)); // doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); @@ -346,7 +346,7 @@ public class TbAlarmNodeTest { when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); when(alarmService.findAlarmByIdAsync(tenantId, id)).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true)); + when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), nullable(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true)); // doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); node.onMsg(ctx, msg); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java index 0c0f49f298..0f5dfb6ee7 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java @@ -22,10 +22,9 @@ import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Matchers; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -66,7 +65,6 @@ public class TbJsFilterNodeTest { public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException { initWithScript(); TbMsg msg = TbMsg.newMsg("USER", null, new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); - mockJsExecutor(); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(false)); node.onMsg(ctx, msg); @@ -79,7 +77,6 @@ public class TbJsFilterNodeTest { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); - mockJsExecutor(); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFailedFuture(new ScriptException("error"))); @@ -92,7 +89,6 @@ public class TbJsFilterNodeTest { initWithScript(); TbMsgMetaData metaData = new TbMsgMetaData(); TbMsg msg = TbMsg.newMsg("USER", null, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); - mockJsExecutor(); when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(true)); node.onMsg(ctx, msg); @@ -112,18 +108,6 @@ public class TbJsFilterNodeTest { node.init(ctx, nodeConfiguration); } - private void mockJsExecutor() { - when(ctx.getJsExecutor()).thenReturn(executor); - doAnswer((Answer>) invocationOnMock -> { - try { - Callable task = (Callable) (invocationOnMock.getArguments())[0]; - return Futures.immediateFuture((Boolean) task.call()); - } catch (Throwable th) { - return Futures.immediateFailedFuture(th); - } - }).when(executor).executeAsync(Matchers.any(Callable.class)); - } - private void verifyError(TbMsg msg, String message, Class expectedClass) { ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); verify(ctx).tellFailure(same(msg), captor.capture()); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java index 5609345ac8..cfacf4cb85 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java @@ -23,9 +23,9 @@ import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Matchers; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -43,7 +43,7 @@ import java.util.Set; import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.same; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -92,6 +92,7 @@ public class TbJsSwitchNodeTest { node.init(ctx, nodeConfiguration); } + @SuppressWarnings("unchecked") private void mockJsExecutor() { when(ctx.getJsExecutor()).thenReturn(executor); doAnswer((Answer>>) invocationOnMock -> { @@ -101,7 +102,7 @@ public class TbJsSwitchNodeTest { } catch (Throwable th) { return Futures.immediateFailedFuture(th); } - }).when(executor).executeAsync(Matchers.any(Callable.class)); + }).when(executor).executeAsync(ArgumentMatchers.any(Callable.class)); } private void verifyError(TbMsg msg, String message, Class expectedClass) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java index 4ded096fb5..3385d20d3a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java @@ -21,7 +21,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; 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 1d811b4ef9..ca88290808 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 @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -58,13 +58,12 @@ import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.same; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE; @RunWith(MockitoJUnitRunner.class) 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 ef8c1c5558..b980423c08 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 @@ -18,26 +18,38 @@ package org.thingsboard.rule.engine.profile; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.AdditionalAnswers; import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +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.EntityType; 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.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +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.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyValueType; @@ -48,12 +60,21 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; +import org.thingsboard.server.dao.model.sql.AttributeKvEntity; import org.thingsboard.server.dao.timeseries.TimeseriesService; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.Optional; import java.util.TreeMap; import java.util.UUID; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) @@ -71,9 +92,14 @@ public class TbDeviceProfileNodeTest { private TimeseriesService timeseriesService; @Mock private RuleEngineAlarmService alarmService; + @Mock + private DeviceService deviceService; + @Mock + private AttributesService attributesService; private TenantId tenantId = new TenantId(UUID.randomUUID()); private DeviceId deviceId = new DeviceId(UUID.randomUUID()); + private CustomerId customerId = new CustomerId(UUID.randomUUID()); private DeviceProfileId deviceProfileId = new DeviceProfileId(UUID.randomUUID()); @Test @@ -121,8 +147,8 @@ public class TbDeviceProfileNodeTest { DeviceProfile deviceProfile = new DeviceProfile(); DeviceProfileData deviceProfileData = new DeviceProfileData(); - KeyFilter highTempFilter = new KeyFilter(); - highTempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); highTempFilter.setValueType(EntityKeyValueType.NUMERIC); NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate(); highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); @@ -137,8 +163,8 @@ public class TbDeviceProfileNodeTest { dpa.setAlarmType("highTemperatureAlarm"); dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); - KeyFilter lowTempFilter = new KeyFilter(); - lowTempFilter.setKey(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); + lowTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); lowTempFilter.setValueType(EntityKeyValueType.NUMERIC); NumericFilterPredicate lowTemperaturePredicate = new NumericFilterPredicate(); lowTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); @@ -183,11 +209,558 @@ public class TbDeviceProfileNodeTest { } + @Test + public void testConstantKeyFilterSimple() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setId(deviceProfileId); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + Device device = new Device(); + device.setId(deviceId); + device.setCustomerId(customerId); + + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey( + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "alarmEnabled" + ); + + AttributeKvEntity attributeKvEntity = new AttributeKvEntity(); + attributeKvEntity.setId(compositeKey); + attributeKvEntity.setBooleanValue(Boolean.TRUE); + attributeKvEntity.setLastUpdateTs(System.currentTimeMillis()); + + AttributeKvEntry entry = attributeKvEntity.toData(); + ListenableFuture> attrListListenableFuture = Futures.immediateFuture(Collections.singletonList(entry)); + + AlarmConditionFilter alarmEnabledFilter = new AlarmConditionFilter(); + alarmEnabledFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.CONSTANT, "alarmEnabled")); + alarmEnabledFilter.setValue(Boolean.TRUE); + alarmEnabledFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate alarmEnabledPredicate = new BooleanFilterPredicate(); + alarmEnabledPredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + alarmEnabledPredicate.setValue(new FilterPredicateValue<>( + Boolean.FALSE, + null, + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "alarmEnabled") + )); + alarmEnabledFilter.setPredicate(alarmEnabledPredicate); + + AlarmConditionFilter temperatureFilter = new AlarmConditionFilter(); + temperatureFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + temperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate temperaturePredicate = new NumericFilterPredicate(); + temperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + temperaturePredicate.setValue(new FilterPredicateValue<>(20.0, null, null)); + temperatureFilter.setPredicate(temperaturePredicate); + + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Arrays.asList(alarmEnabledFilter, temperatureFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("alarmEnabledAlarmID"); + dpa.setAlarmType("alarmEnabledAlarm"); + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) + .thenReturn(attrListListenableFuture); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())) + .thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 21); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testConstantKeyFilterInherited() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setId(deviceProfileId); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + Device device = new Device(); + device.setId(deviceId); + device.setCustomerId(customerId); + + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey( + EntityType.TENANT, tenantId.getId(), "SERVER_SCOPE", "alarmEnabled" + ); + + AttributeKvEntity attributeKvEntity = new AttributeKvEntity(); + attributeKvEntity.setId(compositeKey); + attributeKvEntity.setBooleanValue(Boolean.TRUE); + attributeKvEntity.setLastUpdateTs(System.currentTimeMillis()); + + AttributeKvEntry entry = attributeKvEntity.toData(); + ListenableFuture> attrListListenableFuture = Futures.immediateFuture(Optional.of(entry)); + + AlarmConditionFilter alarmEnabledFilter = new AlarmConditionFilter(); + alarmEnabledFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.CONSTANT, "alarmEnabled")); + alarmEnabledFilter.setValue(Boolean.TRUE); + alarmEnabledFilter.setValueType(EntityKeyValueType.BOOLEAN); + BooleanFilterPredicate alarmEnabledPredicate = new BooleanFilterPredicate(); + alarmEnabledPredicate.setOperation(BooleanFilterPredicate.BooleanOperation.EQUAL); + alarmEnabledPredicate.setValue(new FilterPredicateValue<>( + Boolean.FALSE, + null, + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "alarmEnabled", true) + )); + alarmEnabledFilter.setPredicate(alarmEnabledPredicate); + + AlarmConditionFilter temperatureFilter = new AlarmConditionFilter(); + temperatureFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + temperatureFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate temperaturePredicate = new NumericFilterPredicate(); + temperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + temperaturePredicate.setValue(new FilterPredicateValue<>(20.0, null, null)); + temperatureFilter.setPredicate(temperaturePredicate); + + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Arrays.asList(alarmEnabledFilter, temperatureFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("alarmEnabledAlarmID"); + dpa.setAlarmType("alarmEnabledAlarm"); + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.anyString(), Mockito.anyString())) + .thenReturn(Futures.immediateFuture(Optional.empty())); + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString())) + .thenReturn(attrListListenableFuture); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())) + .thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 21); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testCurrentDeviceAttributeForDynamicValue() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setId(deviceProfileId); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + Device device = new Device(); + device.setId(deviceId); + device.setCustomerId(customerId); + + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey( + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "greaterAttribute" + ); + + AttributeKvEntity attributeKvEntity = new AttributeKvEntity(); + attributeKvEntity.setId(compositeKey); + attributeKvEntity.setLongValue(30L); + attributeKvEntity.setLastUpdateTs(0L); + + AttributeKvEntry entry = attributeKvEntity.toData(); + ListenableFuture> listListenableFutureWithLess = + Futures.immediateFuture(Collections.singletonList(entry)); + + AlarmConditionFilter highTempFilter = new AlarmConditionFilter(); + highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + highTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate(); + highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + highTemperaturePredicate.setValue(new FilterPredicateValue<>( + 0.0, + null, + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "greaterAttribute") + )); + highTempFilter.setPredicate(highTemperaturePredicate); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Collections.singletonList(highTempFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("highTemperatureAlarmID"); + dpa.setAlarmType("highTemperatureAlarm"); + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) + .thenReturn(listListenableFutureWithLess); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())) + .thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 35); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testCurrentCustomersAttributeForDynamicValue() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + deviceProfile.setId(deviceProfileId); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + Device device = new Device(); + device.setId(deviceId); + device.setCustomerId(customerId); + + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey( + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "lessAttribute" + ); + + AttributeKvEntity attributeKvEntity = new AttributeKvEntity(); + attributeKvEntity.setId(compositeKey); + attributeKvEntity.setLongValue(30L); + attributeKvEntity.setLastUpdateTs(0L); + + AttributeKvEntry entry = attributeKvEntity.toData(); + ListenableFuture> listListenableFutureWithLess = + Futures.immediateFuture(Collections.singletonList(entry)); + ListenableFuture> optionalListenableFutureWithLess = + Futures.immediateFuture(Optional.of(entry)); + + AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); + lowTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + lowTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lowTempPredicate = new NumericFilterPredicate(); + lowTempPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); + lowTempPredicate.setValue( + new FilterPredicateValue<>( + 20.0, + null, + new DynamicValue<>(DynamicValueSourceType.CURRENT_CUSTOMER, "lessAttribute")) + ); + lowTempFilter.setPredicate(lowTempPredicate); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("lesstempID"); + dpa.setAlarmType("lessTemperatureAlarm"); + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) + .thenReturn(listListenableFutureWithLess); + Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) + .thenReturn(device); + Mockito.when(attributesService.find(eq(tenantId), eq(customerId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) + .thenReturn(optionalListenableFutureWithLess); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())) + .thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 25); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testCurrentTenantAttributeForDynamicValue() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + Device device = new Device(); + device.setId(deviceId); + device.setCustomerId(customerId); + + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey( + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "lessAttribute" + ); + + AttributeKvEntity attributeKvEntity = new AttributeKvEntity(); + attributeKvEntity.setId(compositeKey); + attributeKvEntity.setLongValue(50L); + attributeKvEntity.setLastUpdateTs(0L); + + AttributeKvEntry entry = attributeKvEntity.toData(); + ListenableFuture> listListenableFutureWithLess = + Futures.immediateFuture(Collections.singletonList(entry)); + ListenableFuture> optionalListenableFutureWithLess = + Futures.immediateFuture(Optional.of(entry)); + + AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); + lowTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + lowTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lowTempPredicate = new NumericFilterPredicate(); + lowTempPredicate.setOperation(NumericFilterPredicate.NumericOperation.LESS); + lowTempPredicate.setValue( + new FilterPredicateValue<>( + 32.0, + null, + new DynamicValue<>(DynamicValueSourceType.CURRENT_TENANT, "lessAttribute")) + ); + lowTempFilter.setPredicate(lowTempPredicate); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("lesstempID"); + dpa.setAlarmType("lessTemperatureAlarm"); + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) + .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) + .thenReturn(listListenableFutureWithLess); + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) + .thenReturn(optionalListenableFutureWithLess); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())) + .thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 40); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + @Test + public void testTenantInheritModeForDynamicValues() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey( + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "tenantAttribute" + ); + + AttributeKvEntity attributeKvEntity = new AttributeKvEntity(); + attributeKvEntity.setId(compositeKey); + attributeKvEntity.setLongValue(100L); + attributeKvEntity.setLastUpdateTs(0L); + + AttributeKvEntry entry = attributeKvEntity.toData(); + ListenableFuture> listListenableFutureWithLess = + Futures.immediateFuture(Collections.singletonList(entry)); + + AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); + lowTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + lowTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lowTempPredicate = new NumericFilterPredicate(); + lowTempPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + lowTempPredicate.setValue( + new FilterPredicateValue<>( + 0.0, + null, + new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "tenantAttribute", true)) + ); + lowTempFilter.setPredicate(lowTempPredicate); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("lesstempID"); + dpa.setAlarmType("lessTemperatureAlarm"); + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) + .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) + .thenReturn(listListenableFutureWithLess); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())) + .thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 150L); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + + + @Test + public void testCustomerInheritModeForDynamicValues() throws Exception { + init(); + + DeviceProfile deviceProfile = new DeviceProfile(); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + + AttributeKvCompositeKey compositeKey = new AttributeKvCompositeKey( + EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "customerAttribute" + ); + + AttributeKvEntity attributeKvEntity = new AttributeKvEntity(); + attributeKvEntity.setId(compositeKey); + attributeKvEntity.setLongValue(100L); + attributeKvEntity.setLastUpdateTs(0L); + + AttributeKvEntry entry = attributeKvEntity.toData(); + ListenableFuture> listListenableFutureWithLess = + Futures.immediateFuture(Collections.singletonList(entry)); + ListenableFuture> optionalListenableFutureWithLess = + Futures.immediateFuture(Optional.of(entry)); + + AlarmConditionFilter lowTempFilter = new AlarmConditionFilter(); + lowTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature")); + lowTempFilter.setValueType(EntityKeyValueType.NUMERIC); + NumericFilterPredicate lowTempPredicate = new NumericFilterPredicate(); + lowTempPredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER); + lowTempPredicate.setValue( + new FilterPredicateValue<>( + 0.0, + null, + new DynamicValue<>(DynamicValueSourceType.CURRENT_CUSTOMER, "customerAttribute", true)) + ); + lowTempFilter.setPredicate(lowTempPredicate); + AlarmCondition alarmCondition = new AlarmCondition(); + alarmCondition.setCondition(Collections.singletonList(lowTempFilter)); + AlarmRule alarmRule = new AlarmRule(); + alarmRule.setCondition(alarmCondition); + DeviceProfileAlarm dpa = new DeviceProfileAlarm(); + dpa.setId("lesstempID"); + dpa.setAlarmType("lessTemperatureAlarm"); + dpa.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule))); + + deviceProfileData.setAlarms(Collections.singletonList(dpa)); + deviceProfile.setProfileData(deviceProfileData); + + Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); + Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) + .thenReturn(Futures.immediateFuture(Collections.emptyList())); + Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) + .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); + Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) + .thenReturn(listListenableFutureWithLess); + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) + .thenReturn(optionalListenableFutureWithLess); + + TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); + Mockito.when(ctx.newMsg(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.anyString())) + .thenReturn(theMsg); + + ObjectNode data = mapper.createObjectNode(); + data.put("temperature", 150L); + TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), + TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); + + node.onMsg(ctx, msg); + verify(ctx).tellSuccess(msg); + verify(ctx).tellNext(theMsg, "Alarm Created"); + verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); + } + private void init() throws TbNodeException { Mockito.when(ctx.getTenantId()).thenReturn(tenantId); Mockito.when(ctx.getDeviceProfileCache()).thenReturn(cache); Mockito.when(ctx.getTimeseriesService()).thenReturn(timeseriesService); Mockito.when(ctx.getAlarmService()).thenReturn(alarmService); + Mockito.when(ctx.getDeviceService()).thenReturn(deviceService); + Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.createObjectNode()); node = new TbDeviceProfileNode(); node.init(ctx, nodeConfiguration); 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 772c17d790..10202e2a68 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 @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -43,9 +43,9 @@ import org.thingsboard.server.dao.asset.AssetService; import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.same; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java index 62558f25d1..ca6a4f8c02 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java @@ -22,9 +22,9 @@ import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Matchers; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -42,12 +42,10 @@ import java.util.Collections; import java.util.concurrent.Callable; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.same; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; @RunWith(MockitoJUnitRunner.class) public class TbTransformMsgNodeTest { @@ -72,7 +70,6 @@ public class TbTransformMsgNodeTest { RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON,rawJson, ruleChainId, ruleNodeId); TbMsg transformedMsg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, "{new}", ruleChainId, ruleNodeId); - mockJsExecutor(); when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFuture(Collections.singletonList(transformedMsg))); node.onMsg(ctx, msg); @@ -93,7 +90,6 @@ public class TbTransformMsgNodeTest { RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); - mockJsExecutor(); when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("error"))); node.onMsg(ctx, msg); @@ -112,18 +108,6 @@ public class TbTransformMsgNodeTest { node.init(ctx, nodeConfiguration); } - private void mockJsExecutor() { - when(ctx.getJsExecutor()).thenReturn(executor); - doAnswer((Answer>) invocationOnMock -> { - try { - Callable task = (Callable) (invocationOnMock.getArguments())[0]; - return Futures.immediateFuture((TbMsg) task.call()); - } catch (Throwable th) { - return Futures.immediateFailedFuture(th); - } - }).when(executor).executeAsync(Matchers.any(Callable.class)); - } - private void verifyError(TbMsg msg, String message, Class expectedClass) { ArgumentCaptor captor = ArgumentCaptor.forClass(Throwable.class); verify(ctx).tellFailure(same(msg), captor.capture()); diff --git a/tools/pom.xml b/tools/pom.xml index 18861cffe7..c7fcf277ea 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard tools diff --git a/tools/src/main/shell/keygen.properties b/tools/src/main/shell/keygen.properties index 0fb36d4524..d2733e5b7b 100644 --- a/tools/src/main/shell/keygen.properties +++ b/tools/src/main/shell/keygen.properties @@ -15,6 +15,7 @@ # DOMAIN_SUFFIX="$(hostname)" +SUBJECT_ALTERNATIVE_NAMES="ip:127.0.0.1" ORGANIZATIONAL_UNIT=Thingsboard ORGANIZATION=Thingsboard CITY=SF diff --git a/tools/src/main/shell/server.keygen.sh b/tools/src/main/shell/server.keygen.sh index e01b17b8b2..7679cbd812 100755 --- a/tools/src/main/shell/server.keygen.sh +++ b/tools/src/main/shell/server.keygen.sh @@ -86,6 +86,12 @@ fi echo "Generating SSL Key Pair..." +EXT="" + +if [[ ! -z "$SUBJECT_ALTERNATIVE_NAMES" ]]; then + EXT="-ext san=$SUBJECT_ALTERNATIVE_NAMES " +fi + keytool -genkeypair -v \ -alias $SERVER_KEY_ALIAS \ -dname "CN=$DOMAIN_SUFFIX, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ @@ -94,7 +100,8 @@ keytool -genkeypair -v \ -storepass $SERVER_KEYSTORE_PASSWORD \ -keyalg $SERVER_KEY_ALG \ -keysize $SERVER_KEY_SIZE \ - -validity 9999 + -validity 9999 \ + $EXT status=$? if [[ $status != 0 ]]; then diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 05eec2e09a..1ede01a491 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.transport @@ -76,7 +76,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/transport/coap/src/main/conf/tb-coap-transport.conf b/transport/coap/src/main/conf/tb-coap-transport.conf index 0f8db67300..73b6e04f68 100644 --- a/transport/coap/src/main/conf/tb-coap-transport.conf +++ b/transport/coap/src/main/conf/tb-coap-transport.conf @@ -14,10 +14,9 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xloggc:@pkg.logFolder@/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=@pkg.logFolder@/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" export LOG_FILENAME=${pkg.name}.out export LOADER_PATH=${pkg.installFolder}/conf diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 252800254f..41ca5683a1 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.transport @@ -76,7 +76,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/transport/http/src/main/conf/tb-http-transport.conf b/transport/http/src/main/conf/tb-http-transport.conf index 0f8db67300..73b6e04f68 100644 --- a/transport/http/src/main/conf/tb-http-transport.conf +++ b/transport/http/src/main/conf/tb-http-transport.conf @@ -14,10 +14,9 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xloggc:@pkg.logFolder@/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=@pkg.logFolder@/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" export LOG_FILENAME=${pkg.name}.out export LOADER_PATH=${pkg.installFolder}/conf diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 7c98fa46dd..585bf50483 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -39,6 +39,7 @@ zk: transport: http: request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}" + max_request_timeout: "${HTTP_MAX_REQUEST_TIMEOUT:300000}" sessions: inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml new file mode 100644 index 0000000000..1b4a68a906 --- /dev/null +++ b/transport/lwm2m/pom.xml @@ -0,0 +1,187 @@ + + + 4.0.0 + + org.thingsboard + 3.3.0-SNAPSHOT + transport + + org.thingsboard.transport + lwm2m + jar + + Thingsboard LwM2m Transport Service + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + java + false + process-resources + package + tb-lwm2m-transport + false + ${project.build.directory}/windows + ThingsBoard LwM2m Transport Service + org.thingsboard.server.lwm2m.ThingsboardLwm2mTransportApplication + + + + + + + + + org.thingsboard.common.transport + lwm2m + + + org.thingsboard.common + queue + + + org.thingsboard.common + cache + + + org.springframework + spring-context-support + + + org.springframework + spring-context + + + org.slf4j + slf4j-api + + + org.slf4j + log4j-over-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.eclipse.leshan + leshan-server-cf + + + org.eclipse.leshan + leshan-client-cf + + + + org.eclipse.leshan + leshan-server-redis + + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.eclipse.californium + californium-core + test-jar + test + + + org.eclipse.californium + californium-core + + + + + + + org.eclipse.californium + element-connector + test-jar + test + + + + + ${pkg.name}-${project.version} + + + ${project.basedir}/src/main/resources + + + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.thingsboard + gradle-maven-plugin + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-install-plugin + + + + + + jenkins + Jenkins Repository + https://repo.jenkins-ci.org/releases + + false + + + + + diff --git a/transport/lwm2m/src/main/conf/logback.xml b/transport/lwm2m/src/main/conf/logback.xml new file mode 100644 index 0000000000..d9818c2257 --- /dev/null +++ b/transport/lwm2m/src/main/conf/logback.xml @@ -0,0 +1,45 @@ + + + + + + + ${pkg.logFolder}/${pkg.name}.log + + ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log + 100MB + 30 + 3GB + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/transport/lwm2m/src/main/conf/tb-lwm2m-transport.conf b/transport/lwm2m/src/main/conf/tb-lwm2m-transport.conf new file mode 100644 index 0000000000..73b6e04f68 --- /dev/null +++ b/transport/lwm2m/src/main/conf/tb-lwm2m-transport.conf @@ -0,0 +1,22 @@ +# +# Copyright © 2016-2021 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. +# + +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=@pkg.logFolder@/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export LOG_FILENAME=${pkg.name}.out +export LOADER_PATH=${pkg.installFolder}/conf diff --git a/transport/lwm2m/src/main/data/credentials/serverKeyStore.jks b/transport/lwm2m/src/main/data/credentials/serverKeyStore.jks new file mode 100644 index 0000000000..9f6748f8fd Binary files /dev/null and b/transport/lwm2m/src/main/data/credentials/serverKeyStore.jks differ diff --git a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh new file mode 100755 index 0000000000..dcf38b4f10 --- /dev/null +++ b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh @@ -0,0 +1,360 @@ +#!/bin/sh +# +# Copyright © 2016-2021 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. +# + +#/home/nick/Igor_project/Thingsboard_Perfrmance_test/performance-tests/src/main/resources/credentials/shell/lwM2M_credentials.sh -p LwX509 -s 0 -f 2000 -a client_alias_ -e client_self_signed_ -b bootstrap -d server -j serverKeyStore.jks -k clientKeyStore.jks -c client_ks_password -w server_ks_password + +#p) CLIENT_CN=$CLIENT_PREFIX00000000 +#s) client_start=0 +#f) client_finish=1 +#a) CLIENT_ALIAS=CLIENT_ALIAS_PREFIX_00000000 +#e) CLIENT_SELF_ALIAS=CLIENT_SELF_ALIAS_PREFIX_00000000 +#b) BOOTSTRAP_ALIAS=bootstrap +#d) SERVER_ALIAS=server +#j) SERVER_STORE=serverKeyStore.jks +#k) CLIENT_STORE=clientKeyStore.jks +#c) CLIENT_STORE_PWD=client_ks_password +#w) SERVER_STORE_PWD=server_ks_password +#l) ROOT_KEY_ALIAS=root_key_alias + +while getopts p:s:f:a:e:b:d:j:k:c:w:l: flag; do + case "${flag}" in + p) client_pref=${OPTARG} ;; + s) client_start=${OPTARG} ;; + f) client_finish=${OPTARG} ;; + a) client_alias_pref=${OPTARG} ;; + e) client_self_alias_pref=${OPTARG} ;; + b) bootstrap_alias=${OPTARG} ;; + d) server_alias=${OPTARG} ;; + j) key_store_server_file=${OPTARG} ;; + k) key_store_client_file=${OPTARG} ;; + c) client_key_store_pwd=${OPTARG} ;; + w) server_key_store_pwd=${OPTARG} ;; + w) root_key_alias=${OPTARG} ;; + esac +done + +# cd to dir of script +script_dir=$(dirname $0) +echo "script_dir: $script_dir" +cd $script_dir +# source the properties: +. ./lwM2M_keygen.properties + +if [ -n "$client_pref" ]; then + CLIENT_PREFIX=$client_pref +fi + +if [ -z "$client_start" ]; then + client_start=0 +fi + +if [ -z "$client_finish" ]; then + client_finish=1 +fi + +if [ -n "$client_alias_pref" ]; then + CLIENT_ALIAS_PREFIX=$client_alias_pref +fi + +if [ -n "$client_self_alias_pref" ]; then + CLIENT_SELF_ALIAS_PREFIX=$client_self_alias_pref +fi + +if [ -n "$bootstrap_alias" ]; then + BOOTSTRAP_ALIAS=$bootstrap_alias +fi + +if [ -n "$server_alias" ]; then + SERVER_ALIAS=$server_alias +fi + +if [ -n "$key_store_server_file" ]; then + SERVER_STORE=$key_store_server_file +fi + +if [ -n "$key_store_client_file" ]; then + CLIENT_STORE=$key_store_client_file +fi + +if [ -n "$client_key_store_pwd" ]; then + CLIENT_STORE_PWD=$client_key_store_pwd +fi + +if [ -n "$server_key_store_pwd" ]; then + SERVER_STORE_PWD=$server_key_store_pwd +fi + +if [ -n "$root_key_alias" ]; then + ROOT_KEY_ALIAS=$root_key_alias +fi + +CLIENT_NUMBER=$client_start + +echo "==Start==" +echo "CLIENT_PREFIX: $CLIENT_PREFIX" +echo "client_start: $client_start" +echo "client_finish: $client_finish" +echo "CLIENT_ALIAS_PREFIX: $CLIENT_ALIAS_PREFIX" +echo "CLIENT_SELF_ALIAS_PREFIX: $CLIENT_SELF_ALIAS_PREFIX" +echo "BOOTSTRAP_ALIAS: $BOOTSTRAP_ALIAS" +echo "SERVER_ALIAS: $SERVER_ALIAS" +echo "SERVER_STORE: $SERVER_STORE" +echo "CLIENT_STORE: $CLIENT_STORE" +echo "CLIENT_STORE_PWD: $CLIENT_STORE_PWD" +echo "SERVER_STORE_PWD: $SERVER_STORE_PWD" +echo "CLIENT_NUMBER: $CLIENT_NUMBER" +echo "ROOT_KEY_ALIAS: $ROOT_KEY_ALIAS" + +end_point() { + echo "$CLIENT_PREFIX$(printf "%08d" $CLIENT_NUMBER)" +} + +client_alias_point() { + echo "$CLIENT_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" +} + +client_self_alias_point() { + echo "$CLIENT_SELF_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" +} + +# Generation of the keystore. +echo "${H0}====START========${RESET}" +echo "${H1}Server Keystore : ${RESET}" +echo "${H1}==================${RESET}" +echo "${H2}Creating the trusted root CA key and certificate...${RESET}" +# -keysize +# 1024 (when using -genkeypair) +keytool \ + -genkeypair \ + -alias $ROOT_KEY_ALIAS \ + -keyalg EC \ + -dname "CN=$ROOT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $SERVER_STORE_PWD \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD + +echo +echo "${H2}Creating server key and self-signed certificate ...${RESET}" +keytool \ + -genkeypair \ + -alias $SERVER_ALIAS \ + -keyalg EC \ + -dname "CN=$SERVER_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $SERVER_STORE_PWD \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD +keytool \ + -exportcert \ + -alias $SERVER_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -importcert \ + -alias $SERVER_SELF_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -noprompt + +echo +echo "${H2}Creating server certificate signed by root CA...${RESET}" +keytool \ + -certreq \ + -alias $SERVER_ALIAS \ + -dname "CN=$SERVER_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -gencert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -storetype $STORETYPE \ + -validity $VALIDITY | + keytool \ + -importcert \ + -alias $SERVER_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD + +echo +echo "${H2}Creating bootstrap key and self-signed certificate ...${RESET}" +keytool \ + -genkeypair \ + -alias $BOOTSTRAP_ALIAS \ + -keyalg EC \ + -dname "CN=$BOOTSTRAP_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $SERVER_STORE_PWD \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD +keytool \ + -exportcert \ + -alias $BOOTSTRAP_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -importcert \ + -alias $BOOTSTRAP_SELF_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -noprompt + +echo +echo "${H2}Creating bootstrap certificate signed by root CA...${RESET}" +keytool \ + -certreq \ + -alias $BOOTSTRAP_ALIAS \ + -dname "CN=$BOOTSTRAP_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -gencert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -storetype $STORETYPE \ + -validity $VALIDITY | + keytool \ + -importcert \ + -alias $BOOTSTRAP_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD + +if [ "$client_start" -lt "$client_finish" ]; then + echo + echo "${H2}Import root certificate just to be able to import need by root CA with expected CN to $CLIENT_STORE${RESET}" + keytool \ + -exportcert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD | + keytool \ + -importcert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD \ + -noprompt +fi + +cert_end_point() { + echo + echo "${H1}Client Keystore : ${RESET}" + echo "${H1}==================${RESET}" + echo "${H2}Creating client key and self-signed certificate with expected CN CLIENT_ALIAS: $CLIENT_ALIAS${RESET}" + keytool \ + -genkeypair \ + -alias $CLIENT_ALIAS \ + -keyalg EC \ + -dname "CN=$CLIENT_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -validity $VALIDITY \ + -storetype $STORETYPE \ + -keypass $CLIENT_STORE_PWD \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD + keytool \ + -exportcert \ + -alias $CLIENT_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD | + keytool \ + -importcert \ + -alias $CLIENT_SELF_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD \ + -noprompt +# +# echo +# echo "${H2}Import root certificate just to be able to import ned by root CA with expected CN...${RESET}" +# keytool \ +# -exportcert \ +# -alias $ROOT_KEY_ALIAS \ +# -keystore $SERVER_STORE \ +# -storepass $SERVER_STORE_PWD | +# keytool \ +# -importcert \ +# -alias $ROOT_KEY_ALIAS \ +# -keystore $CLIENT_STORE \ +# -storepass $CLIENT_STORE_PWD \ +# -noprompt +# + + echo + echo "${H2}Creating client certificate signed by root CA with expected CN CLIENT_ALIAS: $CLIENT_ALIAS CLIENT_CN: $CLIENT_CN${RESET}" + keytool \ + -certreq \ + -alias $CLIENT_ALIAS \ + -dname "CN=$CLIENT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD | + keytool \ + -gencert \ + -alias $ROOT_KEY_ALIAS \ + -keystore $SERVER_STORE \ + -storepass $SERVER_STORE_PWD \ + -storetype $STORETYPE \ + -validity $VALIDITY | + keytool \ + -importcert \ + -alias $CLIENT_ALIAS \ + -keystore $CLIENT_STORE \ + -storepass $CLIENT_STORE_PWD \ + -noprompt +} + +if [ "$client_start" -lt "$client_finish" ]; then + echo "Файл содержит, как минимум, одно слово Bash." + echo + echo "==Start Client==" + while [ "$CLIENT_NUMBER" -lt "$client_finish" ]; do + echo "number $CLIENT_NUMBER" + echo "finish $client_finish" + CLIENT_CN=$(end_point) + CLIENT_ALIAS=$(client_alias_point) + CLIENT_SELF_ALIAS=$(client_self_alias_point) + echo "CLIENT_CN $CLIENT_CN" + echo "CLIENT_ALIAS $CLIENT_ALIAS" + echo "CLIENT_SELF_ALIAS $CLIENT_SELF_ALIAS" + cert_end_point + CLIENT_NUMBER=$(($CLIENT_NUMBER + 1)) + echo + done +fi + +echo +echo "${H0}!!! Warning ${H2}Migrate ${H1}${SERVER_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" +keytool \ + -importkeystore \ + -srckeystore $SERVER_STORE \ + -destkeystore $SERVER_STORE \ + -deststoretype pkcs12 \ + -srcstorepass $SERVER_STORE_PWD + +if [ "$client_start" -lt "$client_finish" ]; then + echo + echo "${H0}!!! Warning ${H2}Migrate ${H1}${CLIENT_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" + keytool \ + -importkeystore \ + -srckeystore $CLIENT_STORE \ + -destkeystore $CLIENT_STORE \ + -deststoretype pkcs12 \ + -srcstorepass $CLIENT_STORE_PWD +fi diff --git a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties new file mode 100644 index 0000000000..7b3cd9c09a --- /dev/null +++ b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties @@ -0,0 +1,57 @@ +# +# Copyright © 2016-2017 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. +# + +# Keystore common parameters +ROOT_KEY_ALIAS=rootCA +DOMAIN_SUFFIX="$(hostname)" +ROOT_CN="$DOMAIN_SUFFIX $ROOT_KEY_ALIAS" +ORGANIZATIONAL_UNIT=Thingsboard +ORGANIZATION=Thingsboard +CITY=SF +STATE_OR_PROVINCE=CA +TWO_LETTER_COUNTRY_CODE=US +VALIDITY=36500 #days +STORETYPE="JKS" + +#Server +SERVER_STORE=serverKeyStore1.jks +SERVER_STORE_PWD=server_ks_password1 +SERVER_ALIAS=server1 +SERVER_CN="$DOMAIN_SUFFIX server LwM2M signed by root CA" +SERVER_SELF_ALIAS=server_self_signed +SERVER_SELF_CN="$DOMAIN_SUFFIX server LwM2M self-signed" +BOOTSTRAP_ALIAS=bootstrap1 +BOOTSTRAP_CN="$DOMAIN_SUFFIX bootstrap server LwM2M signed by root CA" +BOOTSTRAP_SELF_ALIAS=bootstrap_self_signed +BOOTSTRAP_SELF_CN="$DOMAIN_SUFFIX bootstrap server LwM2M self-signed" + +# Client +CLIENT_STORE=clientKeyStore1.jks +CLIENT_STORE_PWD=client_ks_password1 +CLIENT_ALIAS_PREFIX=client_alias_1 +CLIENT_PREFIX=LwX509___ +CLIENT_SELF_ALIAS_PREFIX=client_self_signed_1 +CLIENT_SELF_CN="$DOMAIN_SUFFIX client LwM2M self-signed" + +# Color output stuff +red=`tput setaf 1` +green=`tput setaf 2` +blue=`tput setaf 4` +bold=`tput bold` +H0=${red}${bold} +H1=${green}${bold} +H2=${blue} +RESET=`tput sgr0` diff --git a/transport/lwm2m/src/main/java/org/thingsboard/server/lwm2m/ThingsboardLwm2mTransportApplication.java b/transport/lwm2m/src/main/java/org/thingsboard/server/lwm2m/ThingsboardLwm2mTransportApplication.java new file mode 100644 index 0000000000..90a5282253 --- /dev/null +++ b/transport/lwm2m/src/main/java/org/thingsboard/server/lwm2m/ThingsboardLwm2mTransportApplication.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2021 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.lwm2m; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.util.Arrays; + +@SpringBootConfiguration +@EnableAsync +@EnableScheduling +@ComponentScan({"org.thingsboard.server.lwm2m", "org.thingsboard.server.common", "org.thingsboard.server.transport.lwm2m", "org.thingsboard.server.queue", "org.thingsboard.server.cache"}) +public class ThingsboardLwm2mTransportApplication { + + private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name"; + private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "tb-lwm2m-transport"; + + public static void main(String[] args) { + SpringApplication.run(ThingsboardLwm2mTransportApplication.class, updateArguments(args)); + } + + private static String[] updateArguments(String[] args) { + if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { + String[] modifiedArgs = new String[args.length + 1]; + System.arraycopy(args, 0, modifiedArgs, 0, args.length); + modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM; + return modifiedArgs; + } + return args; + } +} diff --git a/transport/lwm2m/src/main/resources/logback.xml b/transport/lwm2m/src/main/resources/logback.xml new file mode 100644 index 0000000000..7a3d9f6f0d --- /dev/null +++ b/transport/lwm2m/src/main/resources/logback.xml @@ -0,0 +1,36 @@ + + + + + + + + %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + \ No newline at end of file diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml new file mode 100644 index 0000000000..8617868771 --- /dev/null +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -0,0 +1,374 @@ +# +# Copyright © 2016-2021 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. +# + +# If you enabled process metrics you should also enable 'web-environment'. +spring.main.web-environment: "${WEB_APPLICATION_ENABLE:false}" +# If you enabled process metrics you should set 'web-application-type' to 'servlet' value. +spring.main.web-application-type: "${WEB_APPLICATION_TYPE:none}" + +server: + # Server bind address (has no effect if web-environment is disabled). + address: "${HTTP_BIND_ADDRESS:0.0.0.0}" + # Server bind port (has no effect if web-environment is disabled). + port: "${HTTP_BIND_PORT:8083}" + +# Zookeeper connection parameters. Used for service discovery. +zk: + # Enable/disable zookeeper discovery service. + enabled: "${ZOOKEEPER_ENABLED:false}" + # Zookeeper connect string + url: "${ZOOKEEPER_URL:localhost:2181}" + # Zookeeper retry interval in milliseconds + retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}" + # Zookeeper connection timeout in milliseconds + connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}" + # Zookeeper session timeout in milliseconds + session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}" + # Name of the directory in zookeeper 'filesystem' + zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}" + +cache: + # caffeine or redis + type: "${CACHE_TYPE:caffeine}" + +caffeine: + specs: + relations: + timeToLiveInMinutes: 1440 + maxSize: 0 + deviceCredentials: + timeToLiveInMinutes: 1440 + maxSize: 0 + devices: + timeToLiveInMinutes: 1440 + maxSize: 0 + sessions: + timeToLiveInMinutes: 1440 + maxSize: 0 + assets: + timeToLiveInMinutes: 1440 + maxSize: 0 + entityViews: + timeToLiveInMinutes: 1440 + maxSize: 0 + claimDevices: + timeToLiveInMinutes: 1 + maxSize: 0 + securitySettings: + timeToLiveInMinutes: 1440 + maxSize: 0 + tenantProfiles: + timeToLiveInMinutes: 1440 + maxSize: 0 + deviceProfiles: + timeToLiveInMinutes: 1440 + maxSize: 0 + +redis: + # standalone or cluster + connection: + type: "${REDIS_CONNECTION_TYPE:standalone}" + standalone: + host: "${REDIS_HOST:localhost}" + port: "${REDIS_PORT:6379}" + useDefaultClientConfig: "${REDIS_USE_DEFAULT_CLIENT_CONFIG:true}" + # this value may be used only if you used not default ClientConfig + clientName: "${REDIS_CLIENT_NAME:standalone}" + # this value may be used only if you used not default ClientConfig + connectTimeout: "${REDIS_CLIENT_CONNECT_TIMEOUT:30000}" + # this value may be used only if you used not default ClientConfig + readTimeout: "${REDIS_CLIENT_READ_TIMEOUT:60000}" + # this value may be used only if you used not default ClientConfig + usePoolConfig: "${REDIS_CLIENT_USE_POOL_CONFIG:false}" + cluster: + # Comma-separated list of "host:port" pairs to bootstrap from. + nodes: "${REDIS_NODES:}" + # Maximum number of redirects to follow when executing commands across the cluster. + max-redirects: "${REDIS_MAX_REDIRECTS:12}" + useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" + # db index + db: "${REDIS_DB:0}" + # db password + password: "${REDIS_PASSWORD:}" + # pool config + pool_config: + maxTotal: "${REDIS_POOL_CONFIG_MAX_TOTAL:128}" + maxIdle: "${REDIS_POOL_CONFIG_MAX_IDLE:128}" + minIdle: "${REDIS_POOL_CONFIG_MIN_IDLE:16}" + testOnBorrow: "${REDIS_POOL_CONFIG_TEST_ON_BORROW:true}" + testOnReturn: "${REDIS_POOL_CONFIG_TEST_ON_RETURN:true}" + testWhileIdle: "${REDIS_POOL_CONFIG_TEST_WHILE_IDLE:true}" + minEvictableMs: "${REDIS_POOL_CONFIG_MIN_EVICTABLE_MS:60000}" + evictionRunsMs: "${REDIS_POOL_CONFIG_EVICTION_RUNS_MS:30000}" + maxWaitMills: "${REDIS_POOL_CONFIG_MAX_WAIT_MS:60000}" + numberTestsPerEvictionRun: "${REDIS_POOL_CONFIG_NUMBER_TESTS_PER_EVICTION_RUN:3}" + blockWhenExhausted: "${REDIS_POOL_CONFIG_BLOCK_WHEN_EXHAUSTED:true}" + +# LWM2M server parameters +transport: + # Local LwM2M transport parameters + lwm2m: + # Enable/disable lvm2m transport protocol. + enabled: "${LWM2M_ENABLED:true}" + # We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to + # send a Confirmable message to the time when an acknowledgement is no longer expected. + # DEFAULT_TIMEOUT = 2 * 60 * 1000l; 2 min in ms + timeout: "${LWM2M_TIMEOUT:120000}" + # model_path_file: "${LWM2M_MODEL_PATH_FILE:./common/transport/lwm2m/src/main/resources/models/}" + model_path_file: "${LWM2M_MODEL_PATH_FILE:}" + recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" + recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" + request_pool_size: "${LWM2M_REQUEST_POOL_SIZE:100}" + request_error_pool_size: "${LWM2M_REQUEST_ERROR_POOL_SIZE:10}" + registered_pool_size: "${LWM2M_REGISTERED_POOL_SIZE:10}" + update_registered_pool_size: "${LWM2M_UPDATE_REGISTERED_POOL_SIZE:10}" + un_registered_pool_size: "${LWM2M_UN_REGISTERED_POOL_SIZE:10}" + secure: + # Certificate_x509: + # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format + # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh + key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" + # key_store_type: "${LWM2M_KEYSTORE_TYPE:PKCS12}" + # key_store_path_file: "${KEY_STORE_PATH_FILE:/usr/share/thingsboard/conf/credentials/serverKeyStore.jks}" + key_store_path_file: "${KEY_STORE_PATH_FILE:}" + key_store_password: "${LWM2M_KEYSTORE_PASSWORD_SERVER:server_ks_password}" + root_alias: "${LWM2M_SERVER_ROOT_CA:rootca}" + enable_gen_new_key_psk_rpk: "${ENABLE_GEN_NEW_KEY_PSK_RPK:false}" + server: + id: "${LWM2M_SERVER_ID:123}" + bind_address: "${LWM2M_BIND_ADDRESS:0.0.0.0}" + bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC:5685}" + secure: + bind_address_security: "${LWM2M_BIND_ADDRESS_SECURITY:0.0.0.0}" + bind_port_security: "${LWM2M_BIND_PORT_SECURITY:5686}" + # create_rpk: "${CREATE_RPK:}" + # Only for RPK: Public & Private Key. If the keystore file is missing or not working + # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d0301070342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b] + # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b], + # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] + public_x: "${LWM2M_SERVER_PUBLIC_X:05064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f358}" + public_y: "${LWM2M_SERVER_PUBLIC_Y:5eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED:308193020100301306072a8648ce3d020106082a8648ce3d030107047930770201010420dc774b309e547ceb48fee547e104ce201a9c48c449dc5414cd04e7f5cf05f67ba00a06082a8648ce3d030107a1440342000405064b9e6762dd8d8b8a52355d7b4d8b9a3d64e6d2ee277d76c248861353f3585eeb1838e4f9e37b31fa347aef5ce3431eb54e0a2506910c5e0298817445721b}" # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_SERVER:server}" + bootstrap: + enable: "${LWM2M_BOOTSTRAP_ENABLED:true}" + id: "${LWM2M_SERVER_ID:111}" + bind_address: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" + bind_port_no_sec: "${LWM2M_BIND_PORT_NO_SEC_BS:5687}" + secure: + bind_address_security: "${LWM2M_BIND_ADDRESS_BS:0.0.0.0}" + bind_port_security: "${LWM2M_BIND_PORT_SEC_BS:5688}" + # Only for RPK: Public & Private Key. If the keystore file is missing or not working + # - Elliptic Curve parameters : [secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840.10045.3.1.7)] + # - Public Key (Hex): [3059301306072a8648ce3d020106082a8648ce3d030107034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34] + # - Private Key (Hex): [308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34], + public_x: "${LWM2M_SERVER_PUBLIC_X_BS:5017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f91}" + public_y: "${LWM2M_SERVER_PUBLIC_Y_BS:3fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" + private_encoded: "${LWM2M_SERVER_PRIVATE_ENCODED_BS:308193020100301306072a8648ce3d020106082a8648ce3d0301070479307702010104205ecafd90caa7be45c42e1f3f32571632b8409e6e6249d7124f4ba56fab3c8083a00a06082a8648ce3d030107a144034200045017c87a1c1768264656b3b355434b0def6edb8b9bf166a4762d9930cd730f913fc4e61bcd8901ec27c424114c3e887ed372497f0c2cf85839b8443e76988b34}" # Only Certificate_x509: + alias: "${LWM2M_KEYSTORE_ALIAS_BOOTSTRAP:bootstrap}" + # Use redis for Security and Registration stores + redis.enabled: "${LWM2M_REDIS_ENABLED:false}" + + sessions: + inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}" + report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:30000}" + json: + # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON + type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" + # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) + max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + +queue: + type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) + kafka: + bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" + acks: "${TB_KAFKA_ACKS:all}" + retries: "${TB_KAFKA_RETRIES:1}" + batch.size: "${TB_KAFKA_BATCH_SIZE:16384}" + linger.ms: "${TB_KAFKA_LINGER_MS:1}" + buffer.memory: "${TB_BUFFER_MEMORY:33554432}" + replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" + use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" + confluent: + ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" + sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" + sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" + security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" + other: + topic-properties: + rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}" + js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}" + aws_sqs: + use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}" + access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" + secret_access_key: "${TB_QUEUE_AWS_SQS_SECRET_ACCESS_KEY:YOUR_SECRET}" + region: "${TB_QUEUE_AWS_SQS_REGION:YOUR_REGION}" + threads_per_topic: "${TB_QUEUE_AWS_SQS_THREADS_PER_TOPIC:1}" + queue-properties: + rule-engine: "${TB_QUEUE_AWS_SQS_RE_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" + core: "${TB_QUEUE_AWS_SQS_CORE_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" + transport-api: "${TB_QUEUE_AWS_SQS_TA_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" + notifications: "${TB_QUEUE_AWS_SQS_NOTIFICATIONS_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" + js-executor: "${TB_QUEUE_AWS_SQS_JE_QUEUE_PROPERTIES:VisibilityTimeout:30;MaximumMessageSize:262144;MessageRetentionPeriod:604800}" + # VisibilityTimeout in seconds;MaximumMessageSize in bytes;MessageRetentionPeriod in seconds + pubsub: + project_id: "${TB_QUEUE_PUBSUB_PROJECT_ID:YOUR_PROJECT_ID}" + service_account: "${TB_QUEUE_PUBSUB_SERVICE_ACCOUNT:YOUR_SERVICE_ACCOUNT}" + max_msg_size: "${TB_QUEUE_PUBSUB_MAX_MSG_SIZE:1048576}" #in bytes + max_messages: "${TB_QUEUE_PUBSUB_MAX_MESSAGES:1000}" + queue-properties: + rule-engine: "${TB_QUEUE_PUBSUB_RE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" + core: "${TB_QUEUE_PUBSUB_CORE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" + transport-api: "${TB_QUEUE_PUBSUB_TA_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" + notifications: "${TB_QUEUE_PUBSUB_NOTIFICATIONS_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" + js-executor: "${TB_QUEUE_PUBSUB_JE_QUEUE_PROPERTIES:ackDeadlineInSec:30;messageRetentionInSec:604800}" + service_bus: + namespace_name: "${TB_QUEUE_SERVICE_BUS_NAMESPACE_NAME:YOUR_NAMESPACE_NAME}" + sas_key_name: "${TB_QUEUE_SERVICE_BUS_SAS_KEY_NAME:YOUR_SAS_KEY_NAME}" + sas_key: "${TB_QUEUE_SERVICE_BUS_SAS_KEY:YOUR_SAS_KEY}" + max_messages: "${TB_QUEUE_SERVICE_BUS_MAX_MESSAGES:1000}" + queue-properties: + rule-engine: "${TB_QUEUE_SERVICE_BUS_RE_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" + core: "${TB_QUEUE_SERVICE_BUS_CORE_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" + transport-api: "${TB_QUEUE_SERVICE_BUS_TA_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" + notifications: "${TB_QUEUE_SERVICE_BUS_NOTIFICATIONS_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" + js-executor: "${TB_QUEUE_SERVICE_BUS_JE_QUEUE_PROPERTIES:lockDurationInSec:30;maxSizeInMb:1024;messageTimeToLiveInSec:604800}" + rabbitmq: + exchange_name: "${TB_QUEUE_RABBIT_MQ_EXCHANGE_NAME:}" + host: "${TB_QUEUE_RABBIT_MQ_HOST:localhost}" + port: "${TB_QUEUE_RABBIT_MQ_PORT:5672}" + virtual_host: "${TB_QUEUE_RABBIT_MQ_VIRTUAL_HOST:/}" + username: "${TB_QUEUE_RABBIT_MQ_USERNAME:YOUR_USERNAME}" + password: "${TB_QUEUE_RABBIT_MQ_PASSWORD:YOUR_PASSWORD}" + automatic_recovery_enabled: "${TB_QUEUE_RABBIT_MQ_AUTOMATIC_RECOVERY_ENABLED:false}" + connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}" + handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}" + queue-properties: + rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" + core: "${TB_QUEUE_RABBIT_MQ_CORE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" + transport-api: "${TB_QUEUE_RABBIT_MQ_TA_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" + notifications: "${TB_QUEUE_RABBIT_MQ_NOTIFICATIONS_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" + js-executor: "${TB_QUEUE_RABBIT_MQ_JE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" + partitions: + hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" + virtual_nodes_size: "${TB_QUEUE_PARTITIONS_VIRTUAL_NODES_SIZE:16}" + transport_api: + requests_topic: "${TB_QUEUE_TRANSPORT_API_REQUEST_TOPIC:tb_transport.api.requests}" + responses_topic: "${TB_QUEUE_TRANSPORT_API_RESPONSE_TOPIC:tb_transport.api.responses}" + max_pending_requests: "${TB_QUEUE_TRANSPORT_MAX_PENDING_REQUESTS:10000}" + max_requests_timeout: "${TB_QUEUE_TRANSPORT_MAX_REQUEST_TIMEOUT:10000}" + max_callback_threads: "${TB_QUEUE_TRANSPORT_MAX_CALLBACK_THREADS:100}" + request_poll_interval: "${TB_QUEUE_TRANSPORT_REQUEST_POLL_INTERVAL_MS:25}" + response_poll_interval: "${TB_QUEUE_TRANSPORT_RESPONSE_POLL_INTERVAL_MS:25}" + core: + topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" + poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" + partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" + pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:60000}" + stats: + enabled: "${TB_QUEUE_CORE_STATS_ENABLED:false}" + print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:10000}" + js: + # JS Eval request topic + request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" + # JS Eval responses topic prefix that is combined with node id + response_topic_prefix: "${REMOTE_JS_EVAL_RESPONSE_TOPIC:js_eval.responses}" + # JS Eval max pending requests + max_pending_requests: "${REMOTE_JS_MAX_PENDING_REQUESTS:10000}" + # JS Eval max request timeout + max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" + # JS response poll interval + response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" + # JS response auto commit interval + response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}" + rule-engine: + topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" + poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" + pack-processing-timeout: "${TB_QUEUE_RULE_ENGINE_PACK_PROCESSING_TIMEOUT_MS:60000}" + stats: + enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" + print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" + queues: + - name: "${TB_QUEUE_RE_MAIN_QUEUE_NAME:Main}" + topic: "${TB_QUEUE_RE_MAIN_TOPIC:tb_rule_engine.main}" + poll-interval: "${TB_QUEUE_RE_MAIN_POLL_INTERVAL_MS:25}" + partitions: "${TB_QUEUE_RE_MAIN_PARTITIONS:10}" + pack-processing-timeout: "${TB_QUEUE_RE_MAIN_PACK_PROCESSING_TIMEOUT_MS:60000}" + submit-strategy: + type: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL + # For BATCH only + batch-size: "${TB_QUEUE_RE_MAIN_SUBMIT_STRATEGY_BATCH_SIZE:1000}" # Maximum number of messages in batch + processing-strategy: + type: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_TYPE:SKIP_ALL_FAILURES}" # SKIP_ALL_FAILURES, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT + # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT + retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited + failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; + pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; + - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" + topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" + poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" + partitions: "${TB_QUEUE_RE_HP_PARTITIONS:10}" + pack-processing-timeout: "${TB_QUEUE_RE_HP_PACK_PROCESSING_TIMEOUT_MS:60000}" + submit-strategy: + type: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_TYPE:BURST}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL + # For BATCH only + batch-size: "${TB_QUEUE_RE_HP_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch + processing-strategy: + type: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT + # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT + retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited + failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; + pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; + - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" + topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" + poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" + partitions: "${TB_QUEUE_RE_SQ_PARTITIONS:10}" + pack-processing-timeout: "${TB_QUEUE_RE_SQ_PACK_PROCESSING_TIMEOUT_MS:60000}" + submit-strategy: + type: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_TYPE:SEQUENTIAL_BY_ORIGINATOR}" # BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL + # For BATCH only + batch-size: "${TB_QUEUE_RE_SQ_SUBMIT_STRATEGY_BATCH_SIZE:100}" # Maximum number of messages in batch + processing-strategy: + type: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_TYPE:RETRY_FAILED_AND_TIMED_OUT}" # SKIP_ALL_FAILURES, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT + # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT + retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited + failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; + pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; + transport: + # For high priority notifications that require minimum latency and processing time + notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" + poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" + +service: + type: "${TB_SERVICE_TYPE:tb-transport}" + # Unique id for this service (autogenerated if empty) + id: "${TB_SERVICE_ID:}" + tenant_id: "${TB_SERVICE_TENANT_ID:}" # empty or specific tenant id. + + +metrics: + # Enable/disable actuator metrics. + enabled: "${METRICS_ENABLED:false}" + +management: + endpoints: + web: + exposure: + # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). + include: '${METRICS_ENDPOINTS_EXPOSE:info}' diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 1a026f7c32..28aaeeab39 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport org.thingsboard.transport @@ -76,7 +76,7 @@ org.mockito - mockito-all + mockito-core test diff --git a/transport/mqtt/src/main/conf/tb-mqtt-transport.conf b/transport/mqtt/src/main/conf/tb-mqtt-transport.conf index 0f8db67300..73b6e04f68 100644 --- a/transport/mqtt/src/main/conf/tb-mqtt-transport.conf +++ b/transport/mqtt/src/main/conf/tb-mqtt-transport.conf @@ -14,10 +14,9 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xloggc:@pkg.logFolder@/gc.log -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps" -export JAVA_OPTS="$JAVA_OPTS -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10" -export JAVA_OPTS="$JAVA_OPTS -XX:GCLogFileSize=10M -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:CMSWaitDuration=10000 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSParallelInitialMarkEnabled" -export JAVA_OPTS="$JAVA_OPTS -XX:+CMSEdenChunksRecordAlways -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=@pkg.logFolder@/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" +export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" export LOG_FILENAME=${pkg.name}.out export LOADER_PATH=${pkg.installFolder}/conf diff --git a/transport/pom.xml b/transport/pom.xml index 98429f2329..53e1d4a8c7 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard transport @@ -37,6 +37,7 @@ http mqtt coap + lwm2m snmp diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index fbcca16149..ed446a5612 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT transport diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 3cb140c837..590cd73399 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -228,6 +228,7 @@ }, "defaultProject": "thingsboard", "cli": { - "packageManager": "yarn" + "packageManager": "yarn", + "analytics": false } -} +} \ No newline at end of file diff --git a/ui-ngx/package.json b/ui-ngx/package.json index a2449a2a7f..775a685ae8 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "3.2.1", + "version": "3.3.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --open", @@ -68,6 +68,7 @@ "ngx-clipboard": "^13.0.1", "ngx-color-picker": "^10.1.0", "ngx-daterangepicker-material": "^4.0.1", + "ngx-drag-drop": "^2.0.0", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-hm-carousel": "^2.0.0-rc.1", "ngx-sharebuttons": "^8.0.1", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index fdde603242..fb8c809260 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.2.1-SNAPSHOT + 3.3.0-SNAPSHOT thingsboard org.thingsboard diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 8b2c8f0078..5f7474a31b 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -16,7 +16,7 @@ import { AliasInfo, IAliasController, StateControllerHolder, StateEntityInfo } from '@core/api/widget-api.models'; import { forkJoin, Observable, of, ReplaySubject, Subject } from 'rxjs'; -import { Datasource, DatasourceType } from '@app/shared/models/widget.models'; +import { Datasource, DatasourceType, datasourceTypeTranslationMap } from '@app/shared/models/widget.models'; import { deepClone, isEqual } from '@core/utils'; import { EntityService } from '@core/http/entity.service'; import { UtilsService } from '@core/services/utils.service'; @@ -27,6 +27,7 @@ import { defaultEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; +import { TranslateService } from '@ngx-translate/core'; export class AliasController implements IAliasController { @@ -50,11 +51,12 @@ export class AliasController implements IAliasController { constructor(private utils: UtilsService, private entityService: EntityService, + private translate: TranslateService, private stateControllerHolder: StateControllerHolder, private origEntityAliases: EntityAliases, private origFilters: Filters) { - this.entityAliases = deepClone(this.origEntityAliases); - this.filters = deepClone(this.origFilters); + this.entityAliases = deepClone(this.origEntityAliases) || {}; + this.filters = deepClone(this.origFilters) || {}; this.userFilters = {}; } @@ -241,7 +243,7 @@ export class AliasController implements IAliasController { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable { const newDatasource = deepClone(datasource); - if (newDatasource.type === DatasourceType.entity) { + if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount) { if (newDatasource.filterId) { newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); } @@ -329,16 +331,23 @@ export class AliasController implements IAliasController { return forkJoin(observables).pipe( map((result) => { let functionIndex = 0; + let entityCountIndex = 0; result.forEach((datasource) => { - if (datasource.type === DatasourceType.function) { + if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount) { let name: string; if (datasource.name && datasource.name.length) { name = datasource.name; } else { - functionIndex++; - name = DatasourceType.function; - if (functionIndex > 1) { + if (datasource.type === DatasourceType.function) { + functionIndex++; + } else { + entityCountIndex++; + } + name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type)); + if (datasource.type === DatasourceType.function && functionIndex > 1) { name += ' ' + functionIndex; + } else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) { + name += ' ' + entityCountIndex; } } datasource.name = name; diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index b78c0ad029..4437538719 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -71,6 +71,7 @@ export class DataAggregator { private dataReceived = false; private resetPending = false; + private updatedData = false; private noAggregation = this.aggregationType === AggregationType.NONE; private aggregationTimeout = Math.max(this.interval, 1000); @@ -90,7 +91,8 @@ export class DataAggregator { private timeWindow: number, private interval: number, private stateData: boolean, - private utils: UtilsService) { + private utils: UtilsService, + private ignoreDataUpdateOnIntervalTick: boolean) { this.tsKeyNames.forEach((key) => { this.dataBuffer[key] = []; }); @@ -140,6 +142,7 @@ export class DataAggregator { this.elapsed = 0; this.aggregationTimeout = Math.max(this.interval, 1000); this.resetPending = true; + this.updatedData = false; this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); } @@ -180,6 +183,7 @@ export class DataAggregator { this.onInterval(history, detectChanges); } } + this.updatedData = true; } private onInterval(history?: boolean, detectChanges?: boolean) { @@ -201,8 +205,9 @@ export class DataAggregator { } else { this.data = this.updateData(); } - if (this.onDataCb) { + if (this.onDataCb && (!this.ignoreDataUpdateOnIntervalTick || this.updatedData)) { this.onDataCb(this.data, detectChanges); + this.updatedData = false; } if (!history) { this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); @@ -223,6 +228,7 @@ export class DataAggregator { this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; } aggKeyData.delete(aggTimestamp); + this.updatedData = true; } else if (aggTimestamp <= this.endTs) { const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; keyData.push(kvPair); diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 6a8e21a389..dbc9bd2adb 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -22,12 +22,14 @@ import { EntityFilter, EntityKey, EntityKeyType, - entityKeyTypeToDataKeyType, entityPageDataChanged, + entityKeyTypeToDataKeyType, + entityPageDataChanged, KeyFilter, TsValue } from '@shared/models/query/query.models'; import { DataKeyType, + EntityCountCmd, EntityDataCmd, SubscriptionData, SubscriptionDataHolder, @@ -42,6 +44,7 @@ import { DataAggregator } from '@core/api/data-aggregator'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; import { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { EntityId } from '@shared/models/id/entity-id'; import Timeout = NodeJS.Timeout; declare type DataKeyFunction = (time: number, prevValue: any) => any; @@ -66,6 +69,7 @@ export interface EntityDataSubscriptionOptions { type: widgetType; entityFilter?: EntityFilter; isPaginatedDataSubscription?: boolean; + ignoreDataUpdateOnIntervalTick?: boolean; pageLink?: EntityDataPageLink; keyFilters?: Array; additionalKeyFilters?: Array; @@ -82,6 +86,7 @@ export class EntityDataSubscription { private subscriber: TelemetrySubscriber; private dataCommand: EntityDataCmd; private subsCommand: EntityDataCmd; + private countCommand: EntityCountCmd; private attrFields: Array; private tsFields: Array; @@ -91,7 +96,7 @@ export class EntityDataSubscription { private pageData: PageData; private subsTw: SubscriptionTimewindow; private dataAggregators: Array; - private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {} + private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {}; private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; private entityIdToDataIndex: {[id: string]: number}; @@ -125,7 +130,8 @@ export class EntityDataSubscription { } } let key: string; - if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || + this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.datasourceType === DatasourceType.function) { key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`; } else { @@ -150,7 +156,7 @@ export class EntityDataSubscription { clearTimeout(this.timer); this.timer = null; } - if (this.datasourceType === DatasourceType.entity) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount) { if (this.subscriber) { this.subscriber.unsubscribe(); this.subscriber = null; @@ -159,7 +165,7 @@ export class EntityDataSubscription { if (this.dataAggregators) { this.dataAggregators.forEach((aggregator) => { aggregator.destroy(); - }) + }); this.dataAggregators = null; } this.pageData = null; @@ -291,6 +297,79 @@ export class EntityDataSubscription { totalPages: 1 }; this.onPageData(pageData); + } else if (this.datasourceType === DatasourceType.entityCount) { + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.countCommand = new EntityCountCmd(); + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } + this.countCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + keyFilters + }; + this.subscriber.subscriptionCommands.push(this.countCommand); + + const entityId: EntityId = { + id: NULL_UUID, + entityType: null + }; + + const countKey = this.entityDataSubscriptionOptions.dataKeys[0]; + + let dataReceived = false; + + this.subscriber.entityCount$.subscribe( + (entityCountUpdate) => { + if (!dataReceived) { + const entityData: EntityData = { + entityId, + latest: { + [EntityKeyType.ENTITY_FIELD]: { + name: { + ts: Date.now(), + value: DatasourceType.entityCount + } + }, + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now(), + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + dataReceived = true; + } else { + const update: EntityData[] = [{ + entityId, + latest: { + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now(), + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} + }]; + this.onDataUpdate(update); + } + } + ); + this.subscriber.subscribe(); } if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { return of(null); @@ -347,7 +426,7 @@ export class EntityDataSubscription { limit: this.subsTw.aggregation.limit, agg: this.subsTw.aggregation.type, fetchLatestPreviousPoint: this.subsTw.aggregation.stateData - } + }; } } } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { @@ -381,7 +460,7 @@ export class EntityDataSubscription { if (this.dataAggregators) { this.dataAggregators.forEach((aggregator) => { aggregator.destroy(); - }) + }); } this.dataAggregators = []; this.resetData(); @@ -419,7 +498,8 @@ export class EntityDataSubscription { this.datasourceData[dataIndex] = {}; for (const key of Object.keys(this.dataKeys)) { const dataKey = this.dataKeys[key]; - if (this.datasourceType === DatasourceType.entity || this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || + this.entityDataSubscriptionOptions.type === widgetType.timeseries) { const dataKeysList = dataKey as Array; for (let index = 0; index < dataKeysList.length; index++) { this.datasourceData[dataIndex][key + '_' + index] = { @@ -671,7 +751,8 @@ export class EntityDataSubscription { subsTw.aggregation.timeWindow, subsTw.aggregation.interval, subsTw.aggregation.stateData, - this.utils + this.utils, + this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick ); } diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index c02f36a290..6466ba918f 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -60,7 +60,8 @@ export class EntityDataService { constructor(private telemetryService: TelemetryWebsocketService, private utils: UtilsService) {} - public prepareSubscription(listener: EntityDataListener): Observable { + public prepareSubscription(listener: EntityDataListener, + ignoreDataUpdateOnIntervalTick = false): Observable { const datasource = listener.configDatasource; listener.subscriptionOptions = this.createSubscriptionOptions( datasource, @@ -68,7 +69,8 @@ export class EntityDataService { datasource.pageLink, datasource.keyFilters, null, - false); + false, + ignoreDataUpdateOnIntervalTick); if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !datasource.pageLink)) { return of(null); } @@ -87,7 +89,8 @@ export class EntityDataService { public subscribeForPaginatedData(listener: EntityDataListener, pageLink: EntityDataPageLink, - keyFilters: KeyFilter[]): Observable { + keyFilters: KeyFilter[], + ignoreDataUpdateOnIntervalTick = false): Observable { const datasource = listener.configDatasource; listener.subscriptionOptions = this.createSubscriptionOptions( datasource, @@ -95,7 +98,8 @@ export class EntityDataService { pageLink, datasource.keyFilters, keyFilters, - true); + true, + ignoreDataUpdateOnIntervalTick); if (datasource.type === DatasourceType.entity && (!datasource.entityFilter || !pageLink)) { listener.dataLoaded(emptyPageData(), [], listener.configDatasourceIndex, listener.subscriptionOptions.pageLink); @@ -119,7 +123,8 @@ export class EntityDataService { pageLink: EntityDataPageLink, keyFilters: KeyFilter[], additionalKeyFilters: KeyFilter[], - isPaginatedDataSubscription: boolean): EntityDataSubscriptionOptions { + isPaginatedDataSubscription: boolean, + ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions { const subscriptionDataKeys: Array = []; datasource.dataKeys.forEach((dataKey) => { const subscriptionDataKey: SubscriptionDataKey = { @@ -135,13 +140,17 @@ export class EntityDataService { dataKeys: subscriptionDataKeys, type: subscriptionType }; - if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity || + entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) { entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; - entityDataSubscriptionOptions.pageLink = pageLink; entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; + if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { + entityDataSubscriptionOptions.pageLink = pageLink; + } } entityDataSubscriptionOptions.isPaginatedDataSubscription = isPaginatedDataSubscription; + entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick; return entityDataSubscriptionOptions; } } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index b26abdb320..eff95a6f2d 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -53,6 +53,7 @@ import { EntityDataService } from '@core/api/entity-data.service'; import { PageData } from '@shared/models/page/page-data'; import { TranslateService } from '@ngx-translate/core'; import { AlarmDataService } from '@core/api/alarm-data.service'; +import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -137,6 +138,7 @@ export interface StateParams { export type StateControllerHolder = () => IStateController; export interface IStateController { + dashboardCtrl: IDashboardController; getStateParams(): StateParams; getStateParamsByStateId(stateId: string): StateParams; openState(id: string, params?: StateParams, openRightLayout?: boolean): void; @@ -224,6 +226,7 @@ export interface WidgetSubscriptionOptions { hasDataPageLink?: boolean; singleEntity?: boolean; warnOnPageDataOverflow?: boolean; + ignoreDataUpdateOnIntervalTick?: boolean; targetDeviceAliasIds?: Array; targetDeviceIds?: Array; useDashboardTimewindow?: boolean; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 3e3537941b..ce36f57e10 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -83,6 +83,7 @@ export class WidgetSubscription implements IWidgetSubscription { hasDataPageLink: boolean; singleEntity: boolean; warnOnPageDataOverflow: boolean; + ignoreDataUpdateOnIntervalTick: boolean; datasourcePages: PageData[]; dataPages: PageData>[]; @@ -200,6 +201,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.hasDataPageLink = options.hasDataPageLink; this.singleEntity = options.singleEntity; this.warnOnPageDataOverflow = options.warnOnPageDataOverflow; + this.ignoreDataUpdateOnIntervalTick = options.ignoreDataUpdateOnIntervalTick; this.datasourcePages = []; this.datasources = []; this.dataPages = []; @@ -423,7 +425,7 @@ export class WidgetSubscription implements IWidgetSubscription { } }; this.entityDataListeners.push(listener); - return this.ctx.entityDataService.prepareSubscription(listener); + return this.ctx.entityDataService.prepareSubscription(listener, this.ignoreDataUpdateOnIntervalTick); }); return forkJoin(resolveResultObservables).pipe( map((resolveResults) => { @@ -465,7 +467,15 @@ export class WidgetSubscription implements IWidgetSubscription { entityName = this.targetDeviceName; } } else if (this.type === widgetType.alarm) { - if (this.alarms && this.alarms.data.length) { + if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) { + entityId = { + entityType: this.alarmSource.entityType, + id: this.alarmSource.entityId + }; + entityName = this.alarmSource.entityName; + entityLabel = this.alarmSource.entityLabel; + entityDescription = this.alarmSource.entityDescription; + } else if (this.alarms && this.alarms.data.length) { const data = this.alarms.data[0]; entityId = data.originator; entityName = data.originatorName; @@ -815,7 +825,8 @@ export class WidgetSubscription implements IWidgetSubscription { } }; this.entityDataListeners[datasourceIndex] = entityDataListener; - return this.ctx.entityDataService.subscribeForPaginatedData(entityDataListener, pageLink, keyFilters); + return this.ctx.entityDataService.subscribeForPaginatedData(entityDataListener, pageLink, keyFilters, + this.ignoreDataUpdateOnIntervalTick); } else { return of(null); } diff --git a/ui-ngx/src/app/core/guards/auth.guard.ts b/ui-ngx/src/app/core/guards/auth.guard.ts index 06a76e62e1..fdba07b94a 100644 --- a/ui-ngx/src/app/core/guards/auth.guard.ts +++ b/ui-ngx/src/app/core/guards/auth.guard.ts @@ -15,7 +15,7 @@ /// import { Injectable, NgZone } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../auth/auth.service'; import { select, Store } from '@ngrx/store'; import { AppState } from '../core.state'; @@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { UtilsService } from '@core/services/utils.service'; +import { isObject } from '@core/utils'; @Injectable({ providedIn: 'root' @@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service'; export class AuthGuard implements CanActivate, CanActivateChild { constructor(private store: Store, + private router: Router, private authService: AuthService, private dialogService: DialogService, private utils: UtilsService, @@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild { if (data.auth && data.auth.indexOf(authority) === -1) { this.dialogService.forbidden(); return of(false); + } else if (data.redirectTo) { + let redirect; + if (isObject(data.redirectTo)) { + redirect = data.redirectTo[authority]; + } else { + redirect = data.redirectTo; + } + return of(this.router.parseUrl(redirect)); } else { return of(true); } diff --git a/ui-ngx/src/app/core/http/dashboard.service.ts b/ui-ngx/src/app/core/http/dashboard.service.ts index a1307bb080..3f3e512752 100644 --- a/ui-ngx/src/app/core/http/dashboard.service.ts +++ b/ui-ngx/src/app/core/http/dashboard.service.ts @@ -20,7 +20,7 @@ import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; -import { Dashboard, DashboardInfo } from '@shared/models/dashboard.models'; +import { Dashboard, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models'; import { WINDOW } from '@core/services/window.service'; import { NavigationEnd, Router } from '@angular/router'; import { filter, map, publishReplay, refCount } from 'rxjs/operators'; @@ -122,6 +122,19 @@ export class DashboardService { defaultHttpOptionsFromConfig(config)); } + public getHomeDashboard(config?: RequestConfig): Observable { + return this.http.get('/api/dashboard/home', defaultHttpOptionsFromConfig(config)); + } + + public getTenantHomeDashboardInfo(config?: RequestConfig): Observable { + return this.http.get('/api/tenant/dashboard/home/info', defaultHttpOptionsFromConfig(config)); + } + + public setTenantHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo, config?: RequestConfig): Observable { + return this.http.post('/api/tenant/dashboard/home/info', homeDashboardInfo, + defaultHttpOptionsFromConfig(config)); + } + public getPublicDashboardLink(dashboard: DashboardInfo): string | null { if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) { const publicCustomers = dashboard.assignedCustomers diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index ae19504957..e0a0bf5174 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -21,7 +21,9 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isEmptyStr } from '@core/utils'; +import { ObjectLwM2M, ServerSecurityConfig } from '@home/components/profile/device/lwm2m/profile-config.models'; +import { SortOrder } from '@shared/models/page/sort-order'; @Injectable({ providedIn: 'root' @@ -30,7 +32,8 @@ export class DeviceProfileService { constructor( private http: HttpClient - ) { } + ) { + } public getDeviceProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { return this.http.get>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); @@ -40,6 +43,33 @@ export class DeviceProfileService { return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } + public getLwm2mObjects(sortOrder: SortOrder, objectIds?: number[], searchText?: string, config?: RequestConfig): + Observable> { + let url = `/api/lwm2m/deviceProfile/?sortProperty=${sortOrder.property}&sortOrder=${sortOrder.direction}`; + if (isDefinedAndNotNull(objectIds) && objectIds.length > 0) { + url += `&objectIds=${objectIds}`; + } + if (isDefinedAndNotNull(searchText) && !isEmptyStr(searchText)) { + url += `&searchText=${searchText}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + } + + public getLwm2mBootstrapSecurityInfo(securityMode: string, bootstrapServerIs: boolean, + config?: RequestConfig): Observable { + return this.http.get( + `/api/lwm2m/deviceProfile/bootstrap/${securityMode}/${bootstrapServerIs}`, + defaultHttpOptionsFromConfig(config) + ); + } + + public getLwm2mObjectsPage(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>( + `/api/lwm2m/deviceProfile/objects${pageLink.toQuery()}`, + defaultHttpOptionsFromConfig(config) + ); + } + public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable { return this.http.post('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 463183d06e..0862e10a76 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -414,6 +414,7 @@ export class EntityService { { key: nameField, valueType: EntityKeyValueType.STRING, + value: null, predicate: { type: FilterPredicateType.STRING, operation: StringOperation.STARTS_WITH, @@ -480,6 +481,8 @@ export class EntityService { return entityTypes.indexOf(filter.entityType) > -1 ? true : false; case AliasFilterType.entityName: return entityTypes.indexOf(filter.entityType) > -1 ? true : false; + case AliasFilterType.entityType: + return entityTypes.indexOf(filter.entityType) > -1 ? true : false; case AliasFilterType.stateEntity: return true; case AliasFilterType.assetType: @@ -539,6 +542,8 @@ export class EntityService { return true; case AliasFilterType.entityName: return true; + case AliasFilterType.entityType: + return true; case AliasFilterType.stateEntity: return true; case AliasFilterType.assetType: @@ -804,6 +809,9 @@ export class EntityService { case AliasFilterType.entityName: result.entityFilter = deepClone(filter); return of(result); + case AliasFilterType.entityType: + result.entityFilter = deepClone(filter); + return of(result); case AliasFilterType.stateEntity: result.stateEntity = true; if (stateEntityId) { @@ -1098,10 +1106,10 @@ export class EntityService { dataKeys: [] }; this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); - } else if (subscriptionInfo.type === DatasourceType.function) { + } else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount) { datasource = { type: subscriptionInfo.type, - name: subscriptionInfo.name || DatasourceType.function, + name: subscriptionInfo.name || subscriptionInfo.type, dataKeys: [] }; } @@ -1118,6 +1126,10 @@ export class EntityService { if (subscriptionInfo.alarmFields) { this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource); } + if (subscriptionInfo.type === DatasourceType.entityCount) { + const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count); + datasource.dataKeys.push(dataKey); + } } return datasource; } diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index f3614e88d0..cabe78ff6e 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -93,7 +93,7 @@ export class ItemBufferService { if (widget.config.datasources) { for (let i = 0; i < widget.config.datasources.length; i++) { const datasource = widget.config.datasources[i]; - if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { + if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.entityAliasId) { entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId]; if (entityAlias) { aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); @@ -119,7 +119,7 @@ export class ItemBufferService { if (widget.config.datasources) { for (let i = 0; i < widget.config.datasources.length; i++) { const datasource = widget.config.datasources[i]; - if (datasource.type === DatasourceType.entity && datasource.filterId) { + if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.filterId) { filter = dashboard.configuration.filters[datasource.filterId]; if (filter) { filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 98b3b9de1e..d1fffa3fde 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -223,6 +223,7 @@ export class MenuService { name: 'home.home', type: 'link', path: '/home', + notExact: true, icon: 'home' }, { @@ -282,6 +283,13 @@ export class MenuService { path: '/dashboards', icon: 'dashboards' }, + { + id: guid(), + name: 'admin.home-settings', + type: 'link', + path: '/settings/home', + icon: 'settings_applications' + }, { id: guid(), name: 'audit-log.audit-logs', @@ -402,6 +410,7 @@ export class MenuService { name: 'home.home', type: 'link', path: '/home', + notExact: true, icon: 'home' }, { diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 90b2989834..3c1c659e95 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -27,7 +27,8 @@ import { deleteNullProperties, guid, isDefined, - isDefinedAndNotNull, isString, + isDefinedAndNotNull, + isString, isUndefined } from '@core/utils'; import { WindowMessage } from '@shared/models/window-message.model'; diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index ac79c0f04e..c03b10d4d1 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -324,6 +324,9 @@ export function snakeCase(name: string, separator: string): string { } export function getDescendantProp(obj: any, path: string): any { + if (obj.hasOwnProperty(path)) { + return obj[path]; + } return path.split('.').reduce((acc, part) => acc && acc[part], obj); } diff --git a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts index facdd0f60a..a3fc6eb767 100644 --- a/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts +++ b/ui-ngx/src/app/core/ws/telemetry-websocket.service.ts @@ -16,18 +16,27 @@ import { Inject, Injectable, NgZone } from '@angular/core'; import { - AlarmDataCmd, AlarmDataUnsubscribeCmd, + AlarmDataCmd, + AlarmDataUnsubscribeCmd, AlarmDataUpdate, - AttributesSubscriptionCmd, EntityDataCmd, EntityDataUnsubscribeCmd, EntityDataUpdate, - GetHistoryCmd, isAlarmDataUpdateMsg, isEntityDataUpdateMsg, + AttributesSubscriptionCmd, + EntityCountCmd, EntityCountUnsubscribeCmd, + EntityCountUpdate, + EntityDataCmd, + EntityDataUnsubscribeCmd, + EntityDataUpdate, + GetHistoryCmd, + isAlarmDataUpdateMsg, + isEntityCountUpdateMsg, + isEntityDataUpdateMsg, SubscriptionCmd, SubscriptionUpdate, - SubscriptionUpdateMsg, TelemetryFeature, TelemetryPluginCmdsWrapper, TelemetryService, TelemetrySubscriber, - TimeseriesSubscriptionCmd, WebsocketDataMsg + TimeseriesSubscriptionCmd, + WebsocketDataMsg } from '@app/shared/models/telemetry/telemetry.models'; import { select, Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -111,6 +120,8 @@ export class TelemetryWebsocketService implements TelemetryService { this.cmdsWrapper.entityDataCmds.push(subscriptionCommand); } else if (subscriptionCommand instanceof AlarmDataCmd) { this.cmdsWrapper.alarmDataCmds.push(subscriptionCommand); + } else if (subscriptionCommand instanceof EntityCountCmd) { + this.cmdsWrapper.entityCountCmds.push(subscriptionCommand); } } ); @@ -150,6 +161,10 @@ export class TelemetryWebsocketService implements TelemetryService { const alarmDataUnsubscribeCmd = new AlarmDataUnsubscribeCmd(); alarmDataUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; this.cmdsWrapper.alarmDataUnsubscribeCmds.push(alarmDataUnsubscribeCmd); + } else if (subscriptionCommand instanceof EntityCountCmd) { + const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd(); + entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; + this.cmdsWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd); } const cmdId = subscriptionCommand.cmdId; if (cmdId) { @@ -236,7 +251,7 @@ export class TelemetryWebsocketService implements TelemetryService { { url: uri, openObserver: { - next: (e: Event) => { + next: () => { this.onOpen(); } }, @@ -294,6 +309,11 @@ export class TelemetryWebsocketService implements TelemetryService { if (subscriber) { subscriber.onAlarmData(new AlarmDataUpdate(message)); } + } else if (isEntityCountUpdateMsg(message)) { + subscriber = this.subscribersMap.get(message.cmdId); + if (subscriber) { + subscriber.onEntityCount(new EntityCountUpdate(message)); + } } else if (message.subscriptionId) { subscriber = this.subscribersMap.get(message.subscriptionId); if (subscriber) { diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts index f09c80fea4..b7e11982f9 100644 --- a/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts +++ b/ui-ngx/src/app/modules/dashboard/dashboard-pages.module.ts @@ -19,7 +19,6 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module'; -import { DashboardModule } from '@home/pages/dashboard/dashboard.module'; import { DashboardPagesRoutingModule } from './dashboard-pages.routing.module'; @NgModule({ @@ -28,7 +27,6 @@ import { DashboardPagesRoutingModule } from './dashboard-pages.routing.module'; SharedModule, HomeComponentsModule, HomeDialogsModule, - DashboardModule, DashboardPagesRoutingModule ] }) diff --git a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts index 1354f9ef2b..5c3bcb8d11 100644 --- a/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts +++ b/ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts @@ -18,7 +18,7 @@ import { Injectable, NgModule } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/router'; import { Authority } from '@shared/models/authority.enum'; -import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; import { Dashboard } from '@app/shared/models/dashboard.models'; import { DashboardService } from '@core/http/dashboard.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; @@ -95,6 +95,7 @@ const routes: Routes = [ exports: [RouterModule], providers: [ WidgetEditorDashboardResolver, + DashboardResolver, { provide: MODULES_MAP, useValue: modulesMap diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index db12d8ff65..743b49c286 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -43,6 +43,7 @@ import { AlarmDetailsDialogComponent, AlarmDetailsDialogData } from '@home/components/alarm/alarm-details-dialog.component'; +import { DAY, historyInterval } from '@shared/models/time/time.models'; export class AlarmTableConfig extends EntityTableConfig { @@ -59,6 +60,7 @@ export class AlarmTableConfig extends EntityTableConfig this.loadDataOnInit = false; this.tableTitle = ''; this.useTimePageLink = true; + this.defaultTimewindowInterval = historyInterval(DAY * 30); this.detailsPanelEnabled = false; this.selectionEnabled = false; this.searchEnabled = true; diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts index ae6eeaadf9..ad7b3c6a11 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts @@ -14,14 +14,23 @@ /// limitations under the License. /// -import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + Component, + Injector, + Input, + OnDestroy, + OnInit, + StaticProvider, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { TooltipPosition } from '@angular/material/tooltip'; import { AliasInfo, IAliasController } from '@core/api/widget-api.models'; import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; import { BreakpointObserver } from '@angular/cdk/layout'; -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ComponentPortal } from '@angular/cdk/portal'; import { ALIASES_ENTITY_SELECT_PANEL_DATA, AliasesEntitySelectPanelComponent, @@ -136,12 +145,12 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy { overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector)); } - private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): PortalInjector { - const injectionTokens = new WeakMap([ - [ALIASES_ENTITY_SELECT_PANEL_DATA, data], - [OverlayRef, overlayRef] - ]); - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): Injector { + const providers: StaticProvider[] = [ + {provide: ALIASES_ENTITY_SELECT_PANEL_DATA, useValue: data}, + {provide: OverlayRef, useValue: overlayRef} + ]; + return Injector.create({parent: this.viewContainerRef.injector, providers}); } private updateDisplayValue() { diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index ee1fc9c96e..769c8a08af 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -195,7 +195,8 @@ [length]="dataSource.total() | async" [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" - [pageSizeOptions]="[10, 20, 30]"> + [pageSizeOptions]="[10, 20, 30]" + showFirstLastButtons> { overlayRef.dispose(); }); - const injectionTokens = new WeakMap([ - [EDIT_ATTRIBUTE_VALUE_PANEL_DATA, { - attributeValue: attribute.value - } as EditAttributeValuePanelData], - [OverlayRef, overlayRef] - ]); - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + const providers: StaticProvider[] = [ + { + provide: EDIT_ATTRIBUTE_VALUE_PANEL_DATA, + useValue: { + attributeValue: attribute.value + } as EditAttributeValuePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent, this.viewContainerRef, injector)); componentRef.onDestroy(() => { @@ -395,6 +403,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.aliasController = new AliasController(this.utils, this.entityService, + this.translate, () => stateController, entitiAliases, filters); const dataKeyType: DataKeyType = this.attributeScope === LatestTelemetry.LATEST_TELEMETRY ? diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.html rename to ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts similarity index 100% rename from ui-ngx/src/app/modules/home/pages/dashboard/add-widget-dialog.component.ts rename to ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html similarity index 95% rename from ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html rename to ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 5aae43bec6..77e6047d8a 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -21,7 +21,7 @@
-
@@ -49,9 +49,11 @@
@@ -77,7 +79,7 @@ (click)="isFullscreen = !isFullscreen"> {{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }} - - +
+ + device.lwm2m-key + + + {{ 'device.lwm2m-key-required' | translate }} + + + + device.lwm2m-value + + + {{ 'device.lwm2m-value-required' | translate }} + + + {{ 'device.lwm2m-value-json-error' | translate }} + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts index 9b75a2037f..fb4e81653e 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import {Component, forwardRef, Input, OnDestroy, OnInit} from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -33,9 +33,19 @@ import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; -import { Subscription } from 'rxjs'; -import { isDefinedAndNotNull } from '@core/utils'; -import { distinctUntilChanged } from 'rxjs/operators'; +import {Subscription} from 'rxjs'; +import {distinctUntilChanged} from 'rxjs/operators'; +import {SecurityConfigComponent} from '@home/pages/device/lwm2m/security-config.component'; +import { + DEFAULT_END_POINT, + DeviceCredentialsDialogLwm2mData, + END_POINT, + getDefaultSecurityConfig, + JSON_ALL_CONFIG, SecurityConfigModels, validateSecurityConfig +} from '@home/pages/device/lwm2m/security-config.models'; +import {TranslateService} from '@ngx-translate/core'; +import {MatDialog} from '@angular/material/dialog'; +import {isDefinedAndNotNull} from '@core/utils'; @Component({ selector: 'tb-device-credentials', @@ -76,7 +86,9 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, private propagateChange = (v: any) => {}; - constructor(public fb: FormBuilder) { + constructor(public fb: FormBuilder, + private translate: TranslateService, + private dialog: MatDialog) { this.deviceCredentialsFormGroup = this.fb.group({ credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], credentialsId: [null], @@ -190,6 +202,13 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); break; + case DeviceCredentialsType.LWM2M_CREDENTIALS: + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required, this.jsonValidator]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false}); + break; case DeviceCredentialsType.MQTT_BASIC: this.deviceCredentialsFormGroup.get('credentialsBasic').enable({emitEvent: false}); this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity({emitEvent: false}); @@ -197,6 +216,7 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false}); this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false}); + break; } } @@ -223,4 +243,48 @@ export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, onlySelf: true }); } + + openSecurityInfoLwM2mDialog($event: Event): void { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + let credentialsValue = this.deviceCredentialsFormGroup.get('credentialsValue').value; + if (credentialsValue === null || credentialsValue.length === 0) { + credentialsValue = getDefaultSecurityConfig(); + } else { + try { + credentialsValue = JSON.parse(credentialsValue); + } catch (e) { + credentialsValue = getDefaultSecurityConfig(); + } + } + const credentialsId = this.deviceCredentialsFormGroup.get('credentialsId').value || DEFAULT_END_POINT; + this.dialog.open(SecurityConfigComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + jsonAllConfig: credentialsValue, + endPoint: credentialsId + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.deviceCredentialsFormGroup.patchValue({ + credentialsValue: this.isDefautLw2mResponse(res[JSON_ALL_CONFIG]) ? null : JSON.stringify(res[JSON_ALL_CONFIG]), + credentialsId: this.isDefautLw2mResponse(res[END_POINT]) ? null : JSON.stringify(res[END_POINT]).split('\"').join('') + }); + this.deviceCredentialsFormGroup.get('credentialsValue').markAsDirty(); + } + } + ); + } + + private isDefautLw2mResponse(response: object): boolean { + return Object.keys(response).length === 0 || JSON.stringify(response) === '[{}]'; + } + + private jsonValidator(control: FormControl) { + return validateSecurityConfig(control.value) ? null: {jsonError: {parsedJson: "error"}}; + } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index d75500dbda..f4ecd52437 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -238,7 +238,8 @@ [length]="dataSource.total() | async" [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" - [pageSizeOptions]="pageSizeOptions"> + [pageSizeOptions]="pageSizeOptions" + showFirstLastButtons> diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index dedaced162..da735c1567 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -55,11 +55,11 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { DAY, historyInterval, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; +import { HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; -import { HasUUID } from '../../../../shared/models/id/has-uuid'; +import { HasUUID } from '@shared/models/id/has-uuid'; @Component({ selector: 'tb-entities-table', @@ -202,7 +202,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; if (this.entitiesTableConfig.useTimePageLink) { - this.timewindow = historyInterval(DAY); + this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval; const currentTime = Date.now(); this.pageLink = new TimePageLink(10, 0, null, sortOrder, currentTime - this.timewindow.history.timewindowMs, currentTime); @@ -446,7 +446,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn resetSortAndFilter(update: boolean = true, preserveTimewindow: boolean = false) { this.pageLink.textSearch = null; if (this.entitiesTableConfig.useTimePageLink && !preserveTimewindow) { - this.timewindow = historyInterval(DAY); + this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval; } if (this.displayPagination) { this.paginator.pageIndex = 0; diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index 0af4689a18..0eebbc353e 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -44,7 +44,7 @@ import { EntityAction } from '@home/models/entity/entity-component.models'; import { Subscription } from 'rxjs'; import { MatTab, MatTabGroup } from '@angular/material/tabs'; import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; -import { deepClone } from '@core/utils'; +import { deepClone, mergeDeep } from '@core/utils'; @Component({ selector: 'tb-entity-details-panel', @@ -281,6 +281,10 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit saveEntity() { if (this.detailsForm.valid) { const editingEntity = {...this.editingEntity, ...this.entityComponent.entityFormValue()}; + if (this.editingEntity.hasOwnProperty('additionalInfo')) { + editingEntity.additionalInfo = + mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo); + } this.entitiesTableConfig.saveEntity(editingEntity).subscribe( (entity) => { this.entity = entity; diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts index 42fb943ef3..07cee4ffb2 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.ts @@ -76,6 +76,10 @@ export class EntityFilterViewComponent implements ControlValueAccessor { this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith, {prefix}); break; + case AliasFilterType.entityType: + entityType = this.filter.entityType; + this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).typePlural); + break; case AliasFilterType.stateEntity: this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description'); break; 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 cfd75d7299..5c5ca5ff59 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 @@ -59,6 +59,13 @@ + + + + alias.state-entity-parameter-name diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts index 992ee75f7b..539d6b3ff8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts @@ -123,6 +123,11 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit { entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]], }); break; + case AliasFilterType.entityType: + this.filterFormGroup = this.fb.group({ + entityType: [filter ? filter.entityType : null, [Validators.required]] + }); + break; case AliasFilterType.stateEntity: this.filterFormGroup = this.fb.group({ stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html index 6194581625..f7bae7d797 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html @@ -16,7 +16,7 @@ -->
- + @@ -25,7 +25,8 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index fbd329dc16..c6c8a5ed51 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -41,6 +41,8 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On @Input() allowUserDynamicSource = true; + @Input() onlyUserDynamicSource = false; + valueTypeEnum = EntityKeyValueType; booleanFilterPredicateFormGroup: FormGroup; diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html index bbf4887e5f..e2bee07a3f 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html @@ -39,6 +39,7 @@ [valueType]="data.valueType" [displayUserParameters]="data.displayUserParameters" [allowUserDynamicSource]="data.allowUserDynamicSource" + [onlyUserDynamicSource]="data.onlyUserDynamicSource" [operation]="complexFilterFormGroup.get('operation').value" [key]="data.key" formControlName="predicates"> diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts index 447546ba2f..3f1bb3cace 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -37,6 +37,7 @@ export interface ComplexFilterPredicateDialogData { valueType: EntityKeyValueType; displayUserParameters: boolean; allowUserDynamicSource: boolean; + onlyUserDynamicSource: boolean; } @Component({ diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index 0843583a52..9f4acfc3c6 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -52,6 +52,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On @Input() allowUserDynamicSource = true; + @Input() onlyUserDynamicSource = false; + private propagateChange = null; private complexFilterPredicate: ComplexFilterPredicateInfo; @@ -89,7 +91,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On isAdd: false, key: this.key, displayUserParameters: this.displayUserParameters, - allowUserDynamicSource: this.allowUserDynamicSource + allowUserDynamicSource: this.allowUserDynamicSource, + onlyUserDynamicSource: this.onlyUserDynamicSource } }).afterClosed().subscribe( (result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html index 41c04ea238..e5bb27f20c 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html @@ -26,12 +26,12 @@
-
+
- +
@@ -53,6 +53,7 @@ [valueType]="valueType" [displayUserParameters]="displayUserParameters" [allowUserDynamicSource]="allowUserDynamicSource" + [onlyUserDynamicSource]="onlyUserDynamicSource" [key]="key" [formControl]="predicateControl"> diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index e89489e6bb..3c16b01319 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -66,6 +66,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni @Input() allowUserDynamicSource = true; + @Input() onlyUserDynamicSource = false; + filterListFormGroup: FormGroup; valueTypeEnum = EntityKeyValueType; @@ -159,7 +161,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni key: this.key, isAdd: true, displayUserParameters: this.displayUserParameters, - allowUserDynamicSource: this.allowUserDynamicSource + allowUserDynamicSource: this.allowUserDynamicSource, + onlyUserDynamicSource: this.onlyUserDynamicSource } }).afterClosed().pipe( map((result) => { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html index adda4bea1a..4bc249837a 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html @@ -16,7 +16,7 @@ -->
-
+
@@ -45,9 +45,9 @@
filter.default-value
-
+
-
+
@@ -68,9 +68,17 @@
filter.source-attribute
+
+ + {{ 'filter.inherit-owner' | translate}} + +
filter.source-attribute-not-set
+
- + + {{ 'device-profile.lwm2m.valid-id-instance' | translate: { + count: 2, instance: instanceId, min: instanceIdValueMin + } }} + + + {{ 'device-profile.lwm2m.valid-id-instance' | translate: { + count: 1, instance: instanceId, max: instanceIdValueMax + } }} + + + {{ 'device-profile.lwm2m.valid-id-instance' | translate: { + count: 0, instance: instanceId, max: instanceIdValueMax + } }} + + + + diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts new file mode 100644 index 0000000000..1a0ab08e11 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component.ts @@ -0,0 +1,97 @@ +/// +/// Copyright © 2016-2021 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, } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { INSTANCES_ID_VALUE_MAX, INSTANCES_ID_VALUE_MIN, KEY_REGEXP_NUMBER } from './profile-config.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; + +@Component({ + selector: 'tb-profile-lwm2m-object-add-instances-list', + templateUrl: './lwm2m-object-add-instances-list.component.html', + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObjectAddInstancesListComponent), + multi: true + }] +}) +export class Lwm2mObjectAddInstancesListComponent implements ControlValueAccessor { + + private disabled = false; + private dirty = false; + + instancesListFormGroup: FormGroup; + instancesId = new Set(); + instanceIdValueMin = INSTANCES_ID_VALUE_MIN; + instanceIdValueMax = INSTANCES_ID_VALUE_MAX; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder) { + this.instancesListFormGroup = this.fb.group({ + instanceIdInput: [null, [ + Validators.min(this.instanceIdValueMin), + Validators.max(this.instanceIdValueMax), + Validators.pattern(KEY_REGEXP_NUMBER)]] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.instancesListFormGroup.disable({emitEvent: false}); + } else { + this.instancesListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Set): void { + if (value && value.size) { + this.instancesId = value; + } + this.dirty = false; + } + + add = (): void => { + if (this.instancesListFormGroup.get('instanceIdInput').valid && Number.isFinite(Number(this.instanceId))) { + this.instancesId.add(Number(this.instanceId)); + this.instancesListFormGroup.get('instanceIdInput').setValue(null); + this.propagateChange(this.instancesId); + this.dirty = true; + } + } + + remove = (object: number): void => { + this.instancesId.delete(object); + this.propagateChange(this.instancesId); + this.dirty = true; + } + + get instanceId(): number { + return this.instancesListFormGroup.get('instanceIdInput').value; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html new file mode 100644 index 0000000000..34a8d6a843 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.html @@ -0,0 +1,51 @@ + +
+ + {{data.objectName}}    (object [{{data.objectId}}]) + + + + + +
+
+
+ + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.ts new file mode 100644 index 0000000000..3e482120d9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-add-instances.component.ts @@ -0,0 +1,62 @@ +/// +/// Copyright © 2016-2021 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, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface Lwm2mObjectAddInstancesData { + instancesIds: Set; + objectName?: string; + objectId?: number; +} + +@Component({ + selector: 'tb-lwm2m-object-add-instances', + templateUrl: './lwm2m-object-add-instances.component.html' +}) +export class Lwm2mObjectAddInstancesComponent extends DialogComponent implements OnInit { + + instancesFormGroup: FormGroup; + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: Lwm2mObjectAddInstancesData, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.instancesFormGroup = this.fb.group({ + instancesIds: this.data.instancesIds + }); + } + + cancel(): void { + this.dialogRef.close(undefined); + } + + add(): void { + this.data.instancesIds = this.instancesFormGroup.get('instancesIds').value; + this.dialogRef.close(this.data); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html new file mode 100644 index 0000000000..09297c9400 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.html @@ -0,0 +1,55 @@ + + + + + {{objectLwm2m.name}} + close + + + + + + + + + + {{ 'device-profile.lwm2m.no-objects-matching' | translate: {object: searchText} }} + + + + + {{ 'device-profile.lwm2m.object-list-empty' | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts new file mode 100644 index 0000000000..f760c56828 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-object-list.component.ts @@ -0,0 +1,207 @@ +/// +/// Copyright © 2016-2021 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, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Observable } from 'rxjs'; +import { filter, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; +import { ModelValue, ObjectLwM2M } from './profile-config.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { Direction } from '@shared/models/page/sort-order'; +import { isDefined, isDefinedAndNotNull, isEmptyStr, isString } from '@core/utils'; + +@Component({ + selector: 'tb-profile-lwm2m-object-list', + templateUrl: './lwm2m-object-list.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObjectListComponent), + multi: true + }] +}) +export class Lwm2mObjectListComponent implements ControlValueAccessor, OnInit, Validators { + + private requiredValue: boolean; + private dirty = false; + private lw2mModels: Observable>; + private modelValue: Array = []; + + lwm2mListFormGroup: FormGroup; + objectsList: Array = []; + filteredObjectsList: Observable>; + disabled = false; + searchText = ''; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + this.updateValidators(); + } + + @Output() + addList = new EventEmitter(); + + @Output() + removeList = new EventEmitter(); + + @ViewChild('objectInput') objectInput: ElementRef; + + private propagateChange = (v: any) => { + } + + constructor(private store: Store, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder) { + this.lwm2mListFormGroup = this.fb.group({ + objectsList: [this.objectsList], + objectLwm2m: [''] + }); + } + + private updateValidators = (): void => { + this.lwm2mListFormGroup.get('objectLwm2m').setValidators(this.required ? [Validators.required] : []); + this.lwm2mListFormGroup.get('objectLwm2m').updateValueAndValidity(); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredObjectsList = this.lwm2mListFormGroup.get('objectLwm2m').valueChanges + .pipe( + tap((value) => { + if (value && typeof value !== 'string') { + this.add(value); + } else if (value === null) { + this.clear(); + } + }), + filter(searchText => isString(searchText)), + mergeMap(searchText => this.fetchListObjects(searchText)) + ); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.lwm2mListFormGroup.disable({emitEvent: false}); + if (isDefined(this.objectInput)) { + this.clear(); + } + } else { + this.lwm2mListFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: ModelValue): void { + this.searchText = ''; + if (isDefinedAndNotNull(value)) { + if (Array.isArray(value.objectIds)) { + this.modelValue = value.objectIds; + this.objectsList = value.objectsList; + } else { + this.objectsList = []; + this.modelValue = []; + } + this.lwm2mListFormGroup.get('objectsList').setValue(this.objectsList, {emitEvents: false}); + this.dirty = false; + } + } + + private add(object: ObjectLwM2M): void { + if (isDefinedAndNotNull(this.modelValue) && this.modelValue.indexOf(object.id) === -1) { + this.modelValue.push(object.id); + this.objectsList.push(object); + this.lwm2mListFormGroup.get('objectsList').setValue(this.objectsList); + this.addList.next(this.objectsList); + } + this.clear(); + } + + remove = (object: ObjectLwM2M): void => { + let index = this.objectsList.indexOf(object); + if (index >= 0) { + this.objectsList.splice(index, 1); + this.lwm2mListFormGroup.get('objectsList').setValue(this.objectsList); + index = this.modelValue.indexOf(object.id); + this.modelValue.splice(index, 1); + this.removeList.next(object); + this.clear(); + } + } + + displayObjectLwm2mFn = (object?: ObjectLwM2M): string | undefined => { + return object ? object.name : undefined; + } + + private fetchListObjects = (searchText?: string): Observable> => { + this.searchText = searchText; + const filters = {names: [], ids: []}; + if (isDefinedAndNotNull(searchText) && !isEmptyStr(searchText)) { + const ids = searchText.match(/\d+/g); + filters.ids = ids !== null ? ids.map(Number) : filters.ids; + filters.names = searchText.trim().toUpperCase().split(' '); + } + const predicate = objectLwM2M => filters.names.find(word => objectLwM2M.name.toUpperCase().includes(word)) + || filters.ids.includes(objectLwM2M.id); + return this.getLwM2mModels().pipe( + map(objectLwM2Ms => searchText ? objectLwM2Ms.filter(predicate) : objectLwM2Ms) + ); + } + + private getLwM2mModels(): Observable> { + if (!this.lw2mModels) { + const sortOrder = { + property: 'name', + direction: Direction.ASC + }; + this.lw2mModels = this.deviceProfileService.getLwm2mObjects(sortOrder).pipe( + publishReplay(1), + refCount() + ); + } + return this.lw2mModels; + } + + onFocus = (): void => { + if (!this.dirty) { + this.lwm2mListFormGroup.get('objectLwm2m').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = true; + } + } + + private clear = (value: string = ''): void => { + this.objectInput.nativeElement.value = value; + this.searchText = ''; + this.lwm2mListFormGroup.get('objectLwm2m').patchValue(value); + setTimeout(() => { + this.objectInput.nativeElement.blur(); + this.objectInput.nativeElement.focus(); + }, 0); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.html new file mode 100644 index 0000000000..86c232d488 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.html @@ -0,0 +1,83 @@ + +
+
+
+
+
+
+ + device-profile.lwm2m.observe-label + +
+
+ + device-profile.lwm2m.attribute-label + +
+
+ + device-profile.lwm2m.telemetry-label + +
+
+ + device-profile.lwm2m.key-name-label + +
+
+
+
+ Resource[{{resourceLwM2M.get('id').value}}] + name:{{resourceLwM2M.get('name').value}} +
+
+ + +
+
+ + +
+
+ + +
+ + + {{ 'device-profile.lwm2m.key-name_label' | translate }} + + + {{ 'device-profile.lwm2m.key-name' | translate }} + {{ 'device-profile.lwm2m.required' | translate }} + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.ts new file mode 100644 index 0000000000..678137140d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resource.component.ts @@ -0,0 +1,127 @@ +/// +/// Copyright © 2016-2021 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, FormArray, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { ResourceLwM2M } from '@home/components/profile/device/lwm2m/profile-config.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import _ from 'lodash'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; + +@Component({ + selector: 'tb-profile-lwm2m-observe-attr-telemetry-resource', + templateUrl: './lwm2m-observe-attr-telemetry-resource.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryResourceComponent), + multi: true + } + ] +}) + +export class Lwm2mObserveAttrTelemetryResourceComponent implements ControlValueAccessor { + + private requiredValue: boolean; + + resourceFormGroup: FormGroup; + disabled = false; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + } + } + + constructor(private store: Store, + private fb: FormBuilder) { + this.resourceFormGroup = this.fb.group({ + resources: this.fb.array([]) + }); + this.resourceFormGroup.valueChanges.subscribe(value => { + if (!this.disabled) { + this.propagateChangeState(value.resources); + } + }); + } + + registerOnTouched(fn: any): void { + } + + writeValue(value: ResourceLwM2M[]): void { + this.createResourceLwM2M(value); + } + + get resourceFormArray(): FormArray{ + return this.resourceFormGroup.get('resources') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.resourceFormGroup.disable(); + } else { + this.resourceFormGroup.enable(); + } + } + + updateValueKeyName = (event: Event, index: number): void => { + this.resourceFormArray.at(index).patchValue({keyName: _.camelCase((event.target as HTMLInputElement).value)}); + } + + createResourceLwM2M(resourcesLwM2M: ResourceLwM2M[]): void { + if (resourcesLwM2M.length === this.resourceFormArray.length) { + this.resourceFormArray.patchValue(resourcesLwM2M, {emitEvent: false}); + } else { + this.resourceFormArray.clear(); + resourcesLwM2M.forEach(resourceLwM2M => { + this.resourceFormArray.push(this.fb.group({ + id: resourceLwM2M.id, + name: resourceLwM2M.name, + observe: resourceLwM2M.observe, + attribute: resourceLwM2M.attribute, + telemetry: resourceLwM2M.telemetry, + keyName: [resourceLwM2M.keyName, Validators.required] + })); + }); + } + } + + private propagateChange = (v: any) => { }; + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + private propagateChangeState = (value: any): void => { + if (value && this.resourceFormGroup.valid) { + this.propagateChange(value); + } else { + this.propagateChange(null); + } + } + + trackByParams = (index: number): number => { + return index; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css new file mode 100644 index 0000000000..7dc73ee405 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.css @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2021 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. + */ +.vertical-padding { + padding: 0 0 10px 20px; +} + +.left-padding { + padding-left: 5px; +} + +.tb-panel-title-height { + user-select: none; + min-height: 32px; +} + +.checkbox-padding { + padding-left: 22px; +} + +.label-resource { + /*padding: 4px;*/ + color: #002699; + /*text-transform: uppercase;*/ + background: rgba(220, 220, 220, .35); + border-radius: 20px; + width: inherit; + /*margin: 10px 0;*/ + overflow: hidden; + font-size: 15px; + text-overflow: ellipsis; + white-space: nowrap; + opacity: .8; + text-align:center; +} + diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html new file mode 100644 index 0000000000..22f21ce9b7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.html @@ -0,0 +1,104 @@ + +
+ + + + + {{ objectLwM2M.get('name').value}} <id: {{ objectLwM2M.get('id').value}}> + + + + + + +
+ + + +
+
+
+ Instance [{{instances.get('id').value}}] +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts new file mode 100644 index 0000000000..d53e8f241b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component.ts @@ -0,0 +1,300 @@ +/// +/// Copyright © 2016-2021 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 { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormGroup, + NG_VALUE_ACCESSOR, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { CLIENT_LWM2M, Instance, INSTANCES, ObjectLwM2M, ResourceLwM2M, RESOURCES } from './profile-config.models'; +import { deepClone, isDefinedAndNotNull, isEqual, isUndefined } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { + Lwm2mObjectAddInstancesComponent, + Lwm2mObjectAddInstancesData +} from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances.component'; + +@Component({ + selector: 'tb-profile-lwm2m-observe-attr-telemetry', + templateUrl: './lwm2m-observe-attr-telemetry.component.html', + styleUrls: ['./lwm2m-observe-attr-telemetry.component.css'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mObserveAttrTelemetryComponent), + multi: true + } + ] +}) + +export class Lwm2mObserveAttrTelemetryComponent implements ControlValueAccessor { + + private requiredValue: boolean; + + valuePrev = null as any; + observeAttrTelemetryFormGroup: FormGroup; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredValue !== newVal) { + this.requiredValue = newVal; + this.updateValidators(); + } + } + + @Input() + disabled: boolean; + + constructor(private store: Store, + private fb: FormBuilder, + private dialog: MatDialog, + public translate: TranslateService) { + this.observeAttrTelemetryFormGroup = this.fb.group({ + [CLIENT_LWM2M]: this.fb.array([]) + }); + this.observeAttrTelemetryFormGroup.valueChanges.subscribe(value => { + if (isUndefined(this.disabled) || !this.disabled) { + this.propagateChangeState(value); + } + }); + } + + private propagateChange = (v: any) => { + }; + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + private propagateChangeState = (value: any): void => { + if (value) { + if (this.valuePrev === null) { + this.valuePrev = 'init'; + } else if (this.valuePrev === 'init') { + this.valuePrev = value; + } else if (JSON.stringify(value) !== JSON.stringify(this.valuePrev)) { + this.valuePrev = value; + if (this.observeAttrTelemetryFormGroup.valid) { + this.propagateChange(value); + } else { + this.propagateChange(null); + } + } + } + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.valuePrev = null; + if (isDisabled) { + this.observeAttrTelemetryFormGroup.disable(); + } else { + this.observeAttrTelemetryFormGroup.enable(); + } + } + + writeValue(value: {}): void { + if (isDefinedAndNotNull(value)) { + this.buildClientObjectsLwM2M(value[CLIENT_LWM2M]); + } + } + + private buildClientObjectsLwM2M = (objectsLwM2M: ObjectLwM2M []): void => { + this.observeAttrTelemetryFormGroup.setControl(CLIENT_LWM2M, + this.createObjectsLwM2M(objectsLwM2M) + ); + } + + private createObjectsLwM2M = (objectsLwM2M: ObjectLwM2M[]): FormArray => { + return this.fb.array(objectsLwM2M.map((objectLwM2M) => { + return this.fb.group({ + id: objectLwM2M.id, + name: objectLwM2M.name, + multiple: objectLwM2M.multiple, + mandatory: objectLwM2M.mandatory, + instances: this.createInstanceLwM2M(objectLwM2M.instances) + }); + })); + } + + private createInstanceLwM2M = (instancesLwM2M: Instance[]): FormArray => { + return this.fb.array(instancesLwM2M.map((instanceLwM2M) => { + return this.fb.group({ + id: instanceLwM2M.id, + resources: {value: instanceLwM2M.resources, disabled: this.disabled} + }); + })); + } + + get clientLwM2MFormArray(): FormArray { + return this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray; + } + + instancesLwm2mFormArray = (objectLwM2M: AbstractControl): FormArray => { + return objectLwM2M.get(INSTANCES) as FormArray; + } + + changeInstanceResourcesCheckBox = (value: boolean, instance: AbstractControl, type: string): void => { + const resources = deepClone(instance.get(RESOURCES).value as ResourceLwM2M[]); + resources.forEach(resource => resource[type] = value); + instance.get(RESOURCES).patchValue(resources); + this.propagateChange(this.observeAttrTelemetryFormGroup.value); + } + + private updateValidators = (): void => { + this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M).setValidators(this.required ? Validators.required : []); + this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M).updateValueAndValidity(); + } + + trackByParams = (index: number, element: any): number => { + return index; + } + + getIndeterminate = (instance: AbstractControl, type: string): boolean => { + const resources = instance.get(RESOURCES).value as ResourceLwM2M[]; + if (isDefinedAndNotNull(resources)) { + const checkedResource = resources.filter(resource => resource[type]); + return checkedResource.length !== 0 && checkedResource.length !== resources.length; + } + return false; + } + + getChecked = (instance: AbstractControl, type: string): boolean => { + const resources = instance.get(RESOURCES).value as ResourceLwM2M[]; + return isDefinedAndNotNull(resources) && resources.every(resource => resource[type]); + } + + getExpended = (objectLwM2M: AbstractControl): boolean => { + return this.instancesLwm2mFormArray(objectLwM2M).length === 1; + } + + /** + * Instances: indicates whether this Object supports multiple Object Instances or not. + * 1) Field in object: == Multiple/Single + * 2) Field in object: == Mandatory/Optional + * If this field is “Multiple” then the number of Object Instance can be from 0 to many (Object Instance ID MAX_ID=65535). + * If this field is “Single” then the number of Object Instance can be from 0 to 1. (max count == 1) + * If the Object field “Mandatory” is “Mandatory” and the Object field “Instances” is “Single” then - + * the number of Object Instance MUST be 1. + * 1. == Multiple (true), == Optional (false) - + * Object Instance ID MIN_ID=0 MAX_ID=65535 (может ни одного не быть) + * 2. == Multiple (true), == Mandatory (true) - + * Object Instance ID MIN_ID=0 MAX_ID=65535 (min один обязательный) + * 3. == Single (false), == Optional (false) - + * Object Instance ID cnt_max = 1 cnt_min = 0 (может ни одного не быть) + * 4. == Single (false), == Mandatory (true) - + * Object Instance ID cnt_max = cnt_min = 1 (всегда есть один) + */ + + addInstances = ($event: Event, object: ObjectLwM2M): void => { + if ($event) { + $event.stopPropagation(); + $event.preventDefault(); + } + this.dialog.open(Lwm2mObjectAddInstancesComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + instancesIds: this.instancesToSetId(object.instances), + objectName: object.name, + objectId: object.id + } + }).afterClosed().subscribe( + (res: Lwm2mObjectAddInstancesData | undefined) => { + if (isDefinedAndNotNull(res)) { + this.updateInstancesIds(res); + } + } + ); + } + + private updateInstancesIds = (data: Lwm2mObjectAddInstancesData): void => { + const objectLwM2MFormGroup = (this.observeAttrTelemetryFormGroup.get(CLIENT_LWM2M) as FormArray).controls + .find(e => e.value.id === data.objectId) as FormGroup; + const instancesArray = objectLwM2MFormGroup.value.instances as Instance []; + const instancesFormArray = objectLwM2MFormGroup.get(INSTANCES) as FormArray; + const instance0 = deepClone(instancesFormArray.at(0).value as Instance); + instance0.resources.forEach(r => { + r.attribute = false; + r.telemetry = false; + r.observe = false; + }); + const valueOld = this.instancesToSetId(instancesArray); + if (!isEqual(valueOld, data.instancesIds)) { + const idsDel = this.diffBetweenSet(valueOld, data.instancesIds); + const idsAdd = this.diffBetweenSet(data.instancesIds, valueOld); + if (idsAdd.size) { + this.addInstancesNew(idsAdd, objectLwM2MFormGroup, instancesFormArray, instance0); + } + if (idsDel.size) { + this.deleteInstances(idsDel, objectLwM2MFormGroup, instancesFormArray, instance0); + } + } + } + + private addInstancesNew = (idsAdd: Set, objectLwM2MFormGroup: FormGroup, instancesFormArray: FormArray, + instanceNew: Instance): void => { + idsAdd.forEach(x => { + this.pushInstance(instancesFormArray, x, instanceNew); + }); + (instancesFormArray.controls as FormGroup[]).sort((a, b) => a.value.id - b.value.id); + } + + private deleteInstances = (idsDel: Set, objectLwM2MFormGroup: FormGroup, instancesFormArray: FormArray, + instance0: Instance): void => { + idsDel.forEach(x => { + const instanceIndex = instancesFormArray.value.findIndex(element => element.id === x); + instancesFormArray.removeAt(instanceIndex); + }); + if (instancesFormArray.length === 0) { + this.pushInstance(instancesFormArray, 0, instance0); + } + (instancesFormArray.controls as FormGroup[]).sort((a, b) => a.value.id - b.value.id); + } + + private pushInstance = (instancesFormArray: FormArray, x: number, instanceNew: Instance): void => { + instancesFormArray.push(this.fb.group({ + id: x, + resources: {value: instanceNew.resources, disabled: this.disabled} + })); + } + + private diffBetweenSet(firstSet: Set, secondSet: Set): Set { + return new Set([...Array.from(firstSet)].filter(x => !secondSet.has(x))); + } + + private instancesToSetId = (instances: Instance[]): Set => { + return new Set(instances.map(x => x.id)); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts new file mode 100644 index 0000000000..70334c9908 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts @@ -0,0 +1,55 @@ +/// +/// Copyright © 2016-2021 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 { Lwm2mDeviceProfileTransportConfigurationComponent } from './lwm2m-device-profile-transport-configuration.component'; +import { Lwm2mObjectListComponent } from './lwm2m-object-list.component'; +import { Lwm2mObserveAttrTelemetryComponent } from './lwm2m-observe-attr-telemetry.component'; +import { Lwm2mObserveAttrTelemetryResourceComponent } from './lwm2m-observe-attr-telemetry-resource.component'; +import { Lwm2mDeviceConfigServerComponent } from './lwm2m-device-config-server.component'; +import { Lwm2mObjectAddInstancesComponent } from './lwm2m-object-add-instances.component'; +import { Lwm2mObjectAddInstancesListComponent } from './lwm2m-object-add-instances-list.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@app/shared/shared.module'; + +@NgModule({ + declarations: + [ + Lwm2mDeviceProfileTransportConfigurationComponent, + Lwm2mObjectListComponent, + Lwm2mObserveAttrTelemetryComponent, + Lwm2mObserveAttrTelemetryResourceComponent, + Lwm2mDeviceConfigServerComponent, + Lwm2mObjectAddInstancesComponent, + Lwm2mObjectAddInstancesListComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + Lwm2mDeviceProfileTransportConfigurationComponent, + Lwm2mObjectListComponent, + Lwm2mObserveAttrTelemetryComponent, + Lwm2mObserveAttrTelemetryResourceComponent, + Lwm2mDeviceConfigServerComponent, + Lwm2mObjectAddInstancesComponent, + Lwm2mObjectAddInstancesListComponent + ], + providers: [ + ] +}) +export class Lwm2mProfileComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts new file mode 100644 index 0000000000..b4c0e9a539 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/profile-config.models.ts @@ -0,0 +1,198 @@ +/// +/// Copyright © 2016-2021 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. +/// + +export const INSTANCES = 'instances'; +export const RESOURCES = 'resources'; +export const CLIENT_LWM2M = 'clientLwM2M'; +export const CLIENT_LWM2M_SETTINGS = 'clientLwM2mSettings'; +export const OBSERVE_ATTR_TELEMETRY = 'observeAttrTelemetry'; +export const OBSERVE = 'observe'; +export const ATTRIBUTE = 'attribute'; +export const TELEMETRY = 'telemetry'; +export const KEY_NAME = 'keyName'; +export const DEFAULT_ID_SERVER = 123; +export const DEFAULT_ID_BOOTSTRAP = 111; +export const DEFAULT_HOST_NAME = 'localhost'; +export const DEFAULT_PORT_SERVER_NO_SEC = 5685; +export const DEFAULT_PORT_BOOTSTRAP_NO_SEC = 5687; +export const DEFAULT_CLIENT_HOLD_OFF_TIME = 1; +export const DEFAULT_LIFE_TIME = 300; +export const DEFAULT_MIN_PERIOD = 1; +export const DEFAULT_NOTIF_IF_DESIBLED = true; +export const DEFAULT_BINDING = 'U'; +export const DEFAULT_BOOTSTRAP_SERVER_ACCOUNT_TIME_OUT = 0; +export const LEN_MAX_PUBLIC_KEY_RPK = 182; +export const LEN_MAX_PUBLIC_KEY_X509 = 3000; +export const KEY_REGEXP_HEX_DEC = /^[-+]?[0-9A-Fa-f]+\.?[0-9A-Fa-f]*?$/; +export const KEY_REGEXP_NUMBER = /^(\-?|\+?)\d*$/; +export const INSTANCES_ID_VALUE_MIN = 0; +export const INSTANCES_ID_VALUE_MAX = 65535; + +export enum SECURITY_CONFIG_MODE { + PSK = 'PSK', + RPK = 'RPK', + X509 = 'X509', + NO_SEC = 'NO_SEC' +} + +export const SECURITY_CONFIG_MODE_NAMES = new Map( + [ + [SECURITY_CONFIG_MODE.PSK, 'Pre-Shared Key'], + [SECURITY_CONFIG_MODE.RPK, 'Raw Public Key'], + [SECURITY_CONFIG_MODE.X509, 'X.509 Certificate'], + [SECURITY_CONFIG_MODE.NO_SEC, 'No Security'], + ] +); + +export interface ModelValue { + objectIds: number[] | null, + objectsList: ObjectLwM2M[] +} + +export interface BootstrapServersSecurityConfig { + shortId: number; + lifetime: number; + defaultMinPeriod: number; + notifIfDisabled: boolean; + binding: string; +} + +export interface ServerSecurityConfig { + host?: string; + port?: number; + bootstrapServerIs?: boolean; + securityMode: string; + clientPublicKeyOrId?: string; + clientSecretKey?: string; + serverPublicKey?: string; + clientHoldOffTime?: number; + serverId?: number; + bootstrapServerAccountTimeout: number; +} + +interface BootstrapSecurityConfig { + servers: BootstrapServersSecurityConfig; + bootstrapServer: ServerSecurityConfig; + lwm2mServer: ServerSecurityConfig; +} + +export interface ProfileConfigModels { + clientLwM2mSettings: ClientLwM2mSettings; + observeAttr: ObservableAttributes; + bootstrap: BootstrapSecurityConfig; + +} + +export interface ClientLwM2mSettings { + clientOnlyObserveAfterConnect: boolean; +} +export interface ObservableAttributes { + observe: string[]; + attribute: string[]; + telemetry: string[]; + keyName: {}; +} + +export function getDefaultBootstrapServersSecurityConfig(): BootstrapServersSecurityConfig { + return { + shortId: DEFAULT_ID_SERVER, + lifetime: DEFAULT_LIFE_TIME, + defaultMinPeriod: DEFAULT_MIN_PERIOD, + notifIfDisabled: DEFAULT_NOTIF_IF_DESIBLED, + binding: DEFAULT_BINDING + }; +} + +export function getDefaultBootstrapServerSecurityConfig(hostname: any): ServerSecurityConfig { + return { + host: hostname, + port: DEFAULT_PORT_BOOTSTRAP_NO_SEC, + bootstrapServerIs: true, + securityMode: SECURITY_CONFIG_MODE.NO_SEC.toString(), + serverPublicKey: '', + clientHoldOffTime: DEFAULT_CLIENT_HOLD_OFF_TIME, + serverId: DEFAULT_ID_BOOTSTRAP, + bootstrapServerAccountTimeout: DEFAULT_BOOTSTRAP_SERVER_ACCOUNT_TIME_OUT + }; +} + +export function getDefaultLwM2MServerSecurityConfig(hostname): ServerSecurityConfig { + const DefaultLwM2MServerSecurityConfig = getDefaultBootstrapServerSecurityConfig(hostname); + DefaultLwM2MServerSecurityConfig.bootstrapServerIs = false; + DefaultLwM2MServerSecurityConfig.port = DEFAULT_PORT_SERVER_NO_SEC; + DefaultLwM2MServerSecurityConfig.serverId = DEFAULT_ID_SERVER; + return DefaultLwM2MServerSecurityConfig; +} + +function getDefaultProfileBootstrapSecurityConfig(hostname: any): BootstrapSecurityConfig { + return { + servers: getDefaultBootstrapServersSecurityConfig(), + bootstrapServer: getDefaultBootstrapServerSecurityConfig(hostname), + lwm2mServer: getDefaultLwM2MServerSecurityConfig(hostname) + }; +} + +function getDefaultProfileObserveAttrConfig(): ObservableAttributes { + return { + observe: [], + attribute: [], + telemetry: [], + keyName: {} + }; +} + +function getDefaultProfileClientLwM2mSettingsConfig(): ClientLwM2mSettings { + return { + clientOnlyObserveAfterConnect: true + }; +} + +export function getDefaultProfileConfig(hostname?: any): ProfileConfigModels { + return { + clientLwM2mSettings: getDefaultProfileClientLwM2mSettingsConfig(), + observeAttr: getDefaultProfileObserveAttrConfig(), + bootstrap: getDefaultProfileBootstrapSecurityConfig((hostname) ? hostname : DEFAULT_HOST_NAME) + }; +} + +export interface ResourceLwM2M { + id: number; + name: string; + observe: boolean; + attribute: boolean; + telemetry: boolean; + keyName: string; +} + +export interface Instance { + id: number; + resources: ResourceLwM2M[]; +} + +/** + * multiple == true => Multiple + * multiple == false => Single + * mandatory == true => Mandatory + * mandatory == false => Optional + */ +export interface ObjectLwM2M { + id: number; + name: string; + multiple?: boolean; + mandatory?: boolean; + instances?: Instance []; +} + diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts index c9bbf02173..43dbfc45f6 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts @@ -24,7 +24,7 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; -import { EntityTypeFilter } from '@shared/models/relation.models'; +import { RelationEntityTypeFilter } from '@shared/models/relation.models'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -80,7 +80,7 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa this.disabled = isDisabled; } - writeValue(filters: Array): void { + writeValue(filters: Array): void { if (this.valueChangeSubscription) { this.valueChangeSubscription.unsubscribe(); } @@ -102,14 +102,14 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa public addFilter() { const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; - const filter: EntityTypeFilter = { + const filter: RelationEntityTypeFilter = { relationType: null, entityTypes: [] }; relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); } - private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl { + private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl { return this.fb.group({ relationType: [filter ? filter.relationType : null], entityTypes: [filter ? filter.entityTypes : []] @@ -117,7 +117,7 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa } private updateModel() { - const filters: Array = this.relationFiltersFormGroup.get('relationFilters').value; + const filters: Array = this.relationFiltersFormGroup.get('relationFilters').value; this.propagateChange(filters); } } diff --git a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html index b6308223ac..3af73e8932 100644 --- a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.html @@ -18,14 +18,14 @@
admin.number-from - + {{ 'admin.number-from-required' | translate }} - {{ 'admin.phone-number-pattern' | translate }} + {{ 'admin.phone-number-pattern-twilio' | translate }} - + admin.twilio-account-sid @@ -36,7 +36,7 @@ admin.twilio-account-token - + {{ 'admin.twilio-account-token-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts index 759b9448ee..f4c31cd442 100644 --- a/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/sms/twilio-sms-provider-configuration.component.ts @@ -21,8 +21,9 @@ import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { isDefinedAndNotNull } from '@core/utils'; import { - phoneNumberPattern, - SmsProviderConfiguration, SmsProviderType, + phoneNumberPatternTwilio, + SmsProviderConfiguration, + SmsProviderType, TwilioSmsProviderConfiguration } from '@shared/models/settings.models'; @@ -40,7 +41,7 @@ export class TwilioSmsProviderConfigurationComponent implements ControlValueAcce twilioSmsProviderConfigurationFormGroup: FormGroup; - phoneNumberPattern = phoneNumberPattern; + phoneNumberPatternTwilio = phoneNumberPatternTwilio; private requiredValue: boolean; @@ -71,9 +72,9 @@ export class TwilioSmsProviderConfigurationComponent implements ControlValueAcce ngOnInit() { this.twilioSmsProviderConfigurationFormGroup = this.fb.group({ - numberFrom: [null, [Validators.required, Validators.pattern(phoneNumberPattern)]], - accountSid: [null, [Validators.required]], - accountToken: [null, [Validators.required]] + numberFrom: [null, [Validators.required, Validators.pattern(phoneNumberPatternTwilio)]], + accountSid: [null, Validators.required], + accountToken: [null, Validators.required] }); this.twilioSmsProviderConfigurationFormGroup.valueChanges.subscribe(() => { this.updateModel(); diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index 1cfcbd7dd0..33678c88b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -67,15 +67,13 @@
-
-
widget-action.target-dashboard
- -
+
widget-action.target-dashboard
+
-
- - {{ 'widget-action.set-entity-from-widget' | translate }} + + {{ 'widget-action.set-entity-from-widget' | translate }} + + + alias.state-entity-parameter-name + + + + + + {{ 'widget-action.open-in-separate-dialog' | translate }} + +
+ + widget-action.dialog-title + + + + {{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} - - alias.state-entity-parameter-name - + + widget-action.dialog-width + + + {{ 'widget-action.dialog-size-range-error' | translate }} + + + {{ 'widget-action.dialog-size-range-error' | translate }} + + + + widget-action.dialog-height + + + {{ 'widget-action.dialog-size-range-error' | translate }} + + + {{ 'widget-action.dialog-size-range-error' | translate }} + -
+
- - close + close
- + @@ -128,7 +128,7 @@ {{ translate.get('entity.no-key-matching', {key: truncate.transform(searchText, true, 6, '...')}) | async }} - + entity.create-new-key diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index c3f138cb80..96bd8c702f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -16,26 +16,26 @@ import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { - AfterViewInit, - Component, - ElementRef, - forwardRef, - Input, - OnChanges, - OnInit, - SimpleChanges, - SkipSelf, - ViewChild + AfterViewInit, + Component, + ElementRef, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, + SkipSelf, + ViewChild } from '@angular/core'; import { - ControlValueAccessor, - FormBuilder, - FormControl, - FormGroup, - FormGroupDirective, - NG_VALUE_ACCESSOR, - NgForm, - Validators + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR, + NgForm, + Validators } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; @@ -56,8 +56,8 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { DialogService } from '@core/services/dialog.service'; import { MatDialog } from '@angular/material/dialog'; import { - DataKeyConfigDialogComponent, - DataKeyConfigDialogData + DataKeyConfigDialogComponent, + DataKeyConfigDialogData } from '@home/components/widget/data-key-config-dialog.component'; import { deepClone } from '@core/utils'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; @@ -94,8 +94,15 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @Input() datasourceType: DatasourceType; + private maxDataKeysValue: number; + get maxDataKeys(): number { + return this.datasourceType === DatasourceType.entityCount ? 1 : this.maxDataKeysValue; + } + @Input() - maxDataKeys: number; + set maxDataKeys(value: number) { + this.maxDataKeysValue = value; + } @Input() optDataKeys: boolean; @@ -114,7 +121,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie private requiredValue: boolean; get required(): boolean { - return this.requiredValue || !this.optDataKeys; + return this.requiredValue || !this.optDataKeys || this.datasourceType === DatasourceType.entityCount; } @Input() set required(value: boolean) { @@ -210,8 +217,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie if (this.maxDataKeys !== null && this.maxDataKeys > -1) { if (this.datasourceType === DatasourceType.function) { return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); - } else { + } else if (this.datasourceType !== DatasourceType.entityCount) { return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); + } else { + return ''; } } else { return ''; @@ -241,6 +250,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie private reset() { if (this.widgetType === widgetType.alarm) { this.keys = this.utils.getDefaultAlarmDataKeys(); + } else if (this.datasourceType === DatasourceType.entityCount) { + this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count)]; } else { this.keys = []; } @@ -426,7 +437,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; fetchObservable = of(targetKeysList); } else { - if (this.entityAliasId) { + if (this.datasourceType !== DatasourceType.entityCount && this.entityAliasId) { const dataKeyTypes = [DataKeyType.timeseries]; if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { dataKeyTypes.push(DataKeyType.attribute); diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog-token.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog-token.ts new file mode 100644 index 0000000000..7a281c309e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog-token.ts @@ -0,0 +1,22 @@ +/// +/// Copyright © 2016-2021 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 { InjectionToken } from '@angular/core'; +import { ComponentType } from '@angular/cdk/portal'; + +export const EMBED_DASHBOARD_DIALOG_TOKEN: InjectionToken> = + new InjectionToken>('EMBED_DASHBOARD_DIALOG_TOKEN'); + diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html new file mode 100644 index 0000000000..ca4f2a56b7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html @@ -0,0 +1,41 @@ + +
+ +

{{title}}

+ + +
+ + +
+
+ +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.scss new file mode 100644 index 0000000000..b13379a744 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.scss @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2021 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 { + .dashboard-state-dialog { + .mat-dialog-content.dashboard-state-dialog-content { + max-height: 100%; + } + + @media screen and (max-width: 599px) { + width: 100% !important; + height: 100% !important; + } + + @media screen and (min-width: 600px) { + width: 480px; + height: 600px; + } + + @media screen and (min-width: 960px) { + width: 768px; + height: 600px; + } + + @media screen and (min-width: 1280px) { + width: 1000px; + height: 800px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.ts new file mode 100644 index 0000000000..0d12f3c22c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.ts @@ -0,0 +1,79 @@ +/// +/// Copyright © 2016-2021 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, + ComponentFactoryResolver, + Inject, + Injector, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Dashboard } from '@shared/models/dashboard.models'; + +export interface EmbedDashboardDialogData { + dashboard: Dashboard; + state: string; + title: string; + hideToolbar: boolean; + width?: number; + height?: number; +} + +@Component({ + selector: 'tb-embed-dashboard-dialog', + templateUrl: './embed-dashboard-dialog.component.html', + styleUrls: ['./embed-dashboard-dialog.component.scss'] +}) +export class EmbedDashboardDialogComponent extends DialogComponent + implements OnInit { + + @ViewChild('dashboardContent', {read: ViewContainerRef, static: true}) dashboardContentContainer: ViewContainerRef; + + dashboard = this.data.dashboard; + state = this.data.state; + title = this.data.title; + hideToolbar = this.data.hideToolbar; + + dialogStyle: any = {}; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: EmbedDashboardDialogData, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + if (this.data.width) { + this.dialogStyle.width = this.data.width + 'vw'; + } + if (this.data.height) { + this.dialogStyle.height = this.data.height + 'vh'; + } + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(null); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts index e16749191e..6072bf7eaf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts @@ -14,11 +14,22 @@ /// limitations under the License. /// -import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + Component, + forwardRef, + Inject, + Injector, + Input, + OnDestroy, + OnInit, + StaticProvider, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DOCUMENT } from '@angular/common'; import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ComponentPortal } from '@angular/cdk/portal'; import { MediaBreakpoints } from '@shared/models/constants'; import { BreakpointObserver } from '@angular/cdk/layout'; import { WINDOW } from '@core/services/window.service'; @@ -140,12 +151,12 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector)); } - private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector { - const injectionTokens = new WeakMap([ - [LEGEND_CONFIG_PANEL_DATA, data], - [OverlayRef, overlayRef] - ]); - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): Injector { + const providers: StaticProvider[] = [ + {provide: LEGEND_CONFIG_PANEL_DATA, useValue: data}, + {provide: OverlayRef, useValue: overlayRef} + ]; + return Injector.create({parent: this.viewContainerRef.injector, providers}); } registerOnChange(fn: any): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 34dc83f80c..8855e8aa9c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -140,6 +140,7 @@ [length]="alarmsDatasource.total() | async" [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" - [pageSizeOptions]="pageSizeOptions"> + [pageSizeOptions]="pageSizeOptions" + showFirstLastButtons>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index d5ba2aca5b..36d96ee875 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -19,9 +19,11 @@ import { Component, ElementRef, EventEmitter, + Injector, Input, NgZone, OnInit, + StaticProvider, ViewChild, ViewContainerRef } from '@angular/core'; @@ -33,7 +35,7 @@ import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/wi import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber } from '@core/utils'; +import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber, isObject } from '@core/utils'; import cssjs from '@core/css/css'; import { sortItems } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; @@ -64,7 +66,7 @@ import { widthStyle } from '@home/components/widget/lib/table-widget.models'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ComponentPortal } from '@angular/cdk/portal'; import { DISPLAY_COLUMNS_PANEL_DATA, DisplayColumnsPanelComponent, @@ -452,20 +454,26 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, }; }); - const injectionTokens = new WeakMap([ - [DISPLAY_COLUMNS_PANEL_DATA, { - columns, - columnsUpdated: (newColumns) => { - this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def); - if (this.enableSelection) { - this.displayedColumns.unshift('select'); + const providers: StaticProvider[] = [ + { + provide: DISPLAY_COLUMNS_PANEL_DATA, + useValue: { + columns, + columnsUpdated: (newColumns) => { + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def); + if (this.enableSelection) { + this.displayedColumns.unshift('select'); + } + this.displayedColumns.push('actions'); } - this.displayedColumns.push('actions'); - } - } as DisplayColumnsPanelData], - [OverlayRef, overlayRef] - ]); - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + } as DisplayColumnsPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, this.viewContainerRef, injector)); this.ctx.detectChanges(); @@ -492,15 +500,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, overlayRef.backdropClick().subscribe(() => { overlayRef.dispose(); }); - const injectionTokens = new WeakMap([ - [ALARM_FILTER_PANEL_DATA, { - statusList: this.pageLink.statusList, - severityList: this.pageLink.severityList, - typeList: this.pageLink.typeList - } as AlarmFilterPanelData], - [OverlayRef, overlayRef] - ]); - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + const providers: StaticProvider[] = [ + { + provide: ALARM_FILTER_PANEL_DATA, + useValue: { + statusList: this.pageLink.statusList, + severityList: this.pageLink.severityList, + typeList: this.pageLink.typeList + } as AlarmFilterPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent, this.viewContainerRef, injector)); componentRef.onDestroy(() => { @@ -584,8 +598,16 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { try { style = styleInfo.cellStyleFunction(value); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } } catch (e) { style = {}; + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + + `returns '${e}'. Please check your cell style function.`); } } else { style = this.defaultStyle(key, value); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts index 7edd0506b3..e3b51c61cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts @@ -18,9 +18,11 @@ import { Component, Inject, InjectionToken, + Injector, Input, OnDestroy, OnInit, + StaticProvider, ViewChild, ViewContainerRef, ViewEncapsulation @@ -41,7 +43,7 @@ import { import { KeyValue } from '@angular/common'; import * as _moment from 'moment'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ComponentPortal } from '@angular/cdk/portal'; import { MatSelect } from '@angular/material/select'; import { Subscription } from 'rxjs'; import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.models'; @@ -142,18 +144,24 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements overlayRef.backdropClick().subscribe(() => { overlayRef.dispose(); }); - const injectionTokens = new WeakMap([ - [DATE_RANGE_NAVIGATOR_PANEL_DATA, { - model: cloneDateRangeNavigatorModel(this.advancedModel), - settings: this.settings, - onChange: model => { - this.advancedModel = model; - this.triggerChange(); - } - } as DateRangeNavigatorPanelData], - [OverlayRef, overlayRef] - ]); - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + const providers: StaticProvider[] = [ + { + provide: DATE_RANGE_NAVIGATOR_PANEL_DATA, + useValue: { + model: cloneDateRangeNavigatorModel(this.advancedModel), + settings: this.settings, + onChange: model => { + this.advancedModel = model; + this.triggerChange(); + } + } as DateRangeNavigatorPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); overlayRef.attach(new ComponentPortal(DateRangeNavigatorPanelComponent, this.viewContainerRef, injector)); this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts index e79f6ef0e4..5fb11d025b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.component.ts @@ -84,7 +84,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; private datasources: Array; - private data: Array>; private nodesMap: {[nodeId: string]: HierarchyNavTreeNode} = {}; private pendingUpdateNodeTasks: {[nodeId: string]: () => void} = {}; @@ -121,7 +120,6 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O this.widgetConfig = this.ctx.widgetConfig; this.subscription = this.ctx.defaultSubscription; this.datasources = this.subscription.datasources as Array; - this.data = this.subscription.dataPages[0].data; this.initializeConfig(); this.ctx.updateWidgetParams(); } @@ -252,12 +250,16 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O public loadNodes: LoadNodesCallback = (node, cb) => { if (node.id === '#') { const childNodes: HierarchyNavTreeNode[] = []; + let dataIndex = 0; this.datasources.forEach((childDatasource, index) => { - childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, this.data[index])); + const datasourceData = this.subscription.data.slice(dataIndex); + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, datasourceData)); + dataIndex += childDatasource.dataKeys.length; }); cb(this.prepareNodes(childNodes)); } else { - if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.nodeCtx.entity.id.entityType !== 'function') { + if (node.data && node.data.nodeCtx.entity && node.data.nodeCtx.entity.id && node.data.datasource.type === DatasourceType.entity + && node.data.nodeCtx.entity.id.entityType !== 'function') { this.loadChildren(node, node.data.datasource, cb); /* (error) => { // TODO: let errorText = 'Failed to get relations!'; @@ -369,8 +371,8 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O } private datasourceToNode(datasource: HierarchyNodeDatasource, - data: DatasourceData[], - parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { + data: DatasourceData[], + parentNodeCtx?: HierarchyNodeContext): HierarchyNavTreeNode { const node: HierarchyNavTreeNode = { id: (++this.nodeIdCounter) + '' }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts index 07ab91fe5d..74166b1742 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts @@ -153,7 +153,16 @@ export const defaultNodeOpenedFunction: NodeOpenedFunction = nodeCtx => { }; export const defaultNodesSortFunction: NodesSortFunction = (nodeCtx1, nodeCtx2) => { - let result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + let result = 0; + if (!nodeCtx1.entity.id.entityType || !nodeCtx2.entity.id.entityType ) { + if (nodeCtx1.entity.id.entityType) { + result = 1; + } else if (nodeCtx2.entity.id.entityType) { + result = -1; + } + } else { + result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); + } if (result === 0) { result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index eea93478d1..cc6a6a198b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -99,6 +99,7 @@ [length]="entityDatasource.total() | async" [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" - [pageSizeOptions]="pageSizeOptions"> + [pageSizeOptions]="pageSizeOptions" + showFirstLastButtons>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index f75f07ae69..9d1722ad26 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -18,9 +18,11 @@ import { AfterViewInit, Component, ElementRef, + Injector, Input, NgZone, OnInit, + StaticProvider, ViewChild, ViewContainerRef } from '@angular/core'; @@ -38,7 +40,7 @@ import { import { IWidgetSubscription } from '@core/api/widget-api.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber } from '@core/utils'; +import { createLabelFromDatasource, deepClone, hashCode, isDefined, isNumber, isObject } from '@core/utils'; import cssjs from '@core/css/css'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; @@ -70,7 +72,7 @@ import { widthStyle } from '@home/components/widget/lib/table-widget.models'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ComponentPortal } from '@angular/cdk/portal'; import { DISPLAY_COLUMNS_PANEL_DATA, DisplayColumnsPanelComponent, @@ -422,17 +424,23 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni }; }); - const injectionTokens = new WeakMap([ - [DISPLAY_COLUMNS_PANEL_DATA, { - columns, - columnsUpdated: (newColumns) => { - this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def); - this.displayedColumns.push('actions'); - } - } as DisplayColumnsPanelData], - [OverlayRef, overlayRef] - ]); - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + const providers: StaticProvider[] = [ + { + provide: DISPLAY_COLUMNS_PANEL_DATA, + useValue: { + columns, + columnsUpdated: (newColumns) => { + this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def); + this.displayedColumns.push('actions'); + } + } as DisplayColumnsPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent, this.viewContainerRef, injector)); this.ctx.detectChanges(); @@ -507,8 +515,16 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { try { style = styleInfo.cellStyleFunction(value); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } } catch (e) { style = {}; + console.warn(`Cell style function for data key '${key.label}' in widget '${this.ctx.widgetTitle}' ` + + `returns '${e}'. Please check your cell style function.`); } } else { style = {}; @@ -530,7 +546,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni try { content = contentInfo.cellContentFunction(value, entity, this.ctx); } catch (e) { - content = '' + value; + content = '' + value; } } else { content = this.defaultContent(key, contentInfo, value); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index 390ac26d5c..4850d08723 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -175,7 +175,7 @@ export class TbFlot { autoHighlight: this.tooltipIndividual === true, markings: [] }, - selection : { mode : ctx.isMobile ? null : 'x' }, + selection : { mode : 'x' }, legend : { show: false } @@ -702,7 +702,7 @@ export class TbFlot { } public checkMouseEvents() { - const enabled = !this.ctx.isMobile && !this.ctx.isEdit; + const enabled = !this.ctx.isEdit; if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) { this.mouseEventsEnabled = enabled; if (this.$element) { @@ -1289,6 +1289,7 @@ export class TbFlot { let value: any; let lastValue = 0; let minDistanceHistorical: number; + let deltaX = 0; const results: TbFlotHoverInfo[] = [{ seriesHover: [] }]; @@ -1297,6 +1298,13 @@ export class TbFlot { seriesHover: [] }); } + if (this.chartType === 'bar' && this.options.series.bars.align !== 'left') { + if (this.options.series.bars.align === 'center') { + deltaX = this.options.series.bars.barWidth / 2; + } else { + deltaX = this.options.series.bars.barWidth; + } + } for (i = 0; i < seriesList.length; i++) { series = seriesList[i]; let posx: number; @@ -1305,6 +1313,7 @@ export class TbFlot { } else { posx = pos.x; } + posx += deltaX; hoverIndex = this.findHoverIndexFromData(posx, series); if (series.data[hoverIndex] && series.data[hoverIndex][0]) { hoverDistance = posx - series.data[hoverIndex][0]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 57a0563a4e..9da5e679f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -626,10 +626,14 @@ export default abstract class LeafletMap { } this.points = new FeatureGroup(); } + let pointColor = this.options.pointColor; for (const pointsList of pointsData) { pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { + if (this.options.useColorPointFunction) { + pointColor = safeExecute(this.options.colorPointFunction, [data, pointsData, data.dsIndex]); + } const point = L.circleMarker(this.convertPosition(data), { - color: this.options.pointColor, + color: pointColor, radius: this.options.pointSize }); if (!this.options.pointTooltipOnRightPanel) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index 45830cbd55..cbf5883659 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -201,6 +201,8 @@ export type TripAnimationSettings = { pointAsAnchorFunction: GenericFunction; tooltipFunction: GenericFunction; labelFunction: GenericFunction; + useColorPointFunction: boolean; + colorPointFunction: GenericFunction; }; export type actionsHandler = ($event: Event, datasource: Datasource) => void; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index e1cad7a8f7..3a48e8e20f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -301,6 +301,7 @@ export class MapWidgetController implements MapWidgetInterface { labelFunction: parseFunction(settings.labelFunction, functionParams), tooltipFunction: parseFunction(settings.tooltipFunction, functionParams), colorFunction: parseFunction(settings.colorFunction, functionParams), + colorPointFunction: parseFunction(settings.colorPointFunction, functionParams), polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams), markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index 9e1ce715ff..b00e01404a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -871,6 +871,15 @@ export const pointSchema = title: 'Point color', type: 'string' }, + useColorPointFunction: { + title: 'Use color point function', + type: 'boolean', + default: false + }, + colorPointFunction: { + title: 'Color point function: f(data, dsData, dsIndex)', + type: 'string' + }, pointSize: { title: 'Point size (px)', type: 'number', @@ -899,6 +908,11 @@ export const pointSchema = key: 'pointColor', type: 'color' }, + 'useColorPointFunction', + { + key: 'colorPointFunction', + type: 'javascript' + }, 'pointSize', 'usePointAsAnchor', { diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html similarity index 68% rename from ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html index f8b9da9948..ec82ff149e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html @@ -15,10 +15,7 @@ limitations under the License. --> - - - - + + {{settings.icon}} + {{translatedName}} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss new file mode 100644 index 0000000000..18a81aa216 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2021 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 { + width: 100%; + height: 100%; +} + +:host ::ng-deep { + .tb-nav-button { + width: 100%; + height: 100%; + &:hover { + border-bottom: none; + } + &:focus { + border-bottom: none; + } + .mat-button-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + mat-icon { + margin: auto; + } + span { + height: 18px; + min-height: 36px; + max-height: 36px; + padding: 0 0 20px 0; + margin: auto; + font-size: 18px; + font-weight: 400; + line-height: 18px; + white-space: normal; + } + } + &.mat-raised-button.mat-primary { + .mat-ripple-element { + opacity: 0.3; + background-color: rgba(255, 255, 255, 0.3); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts new file mode 100644 index 0000000000..5dbd8022ce --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts @@ -0,0 +1,66 @@ +/// +/// Copyright © 2016-2021 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 { PageComponent } from '@shared/components/page.component'; +import { Component, Input, NgZone, OnInit } from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { UtilsService } from '@core/services/utils.service'; + +interface NavigationCardWidgetSettings { + name: string; + icon: string; + path: string; +} + +@Component({ + selector: 'tb-navigation-card-widget', + templateUrl: './navigation-card-widget.component.html', + styleUrls: ['./navigation-card-widget.component.scss'] +}) +export class NavigationCardWidgetComponent extends PageComponent implements OnInit { + + settings: NavigationCardWidgetSettings; + + translatedName: string; + + @Input() + ctx: WidgetContext; + + constructor(protected store: Store, + private utils: UtilsService, + private ngZone: NgZone, + private router: Router) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.navigationCardWidget = this; + this.settings = this.ctx.settings; + this.translatedName = this.utils.customTranslation(this.settings.name, this.settings.name); + } + + + navigate($event: Event, path: string) { + $event.preventDefault(); + this.ngZone.run(() => { + this.router.navigateByUrl(path); + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html new file mode 100644 index 0000000000..a9ed8e76bb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html @@ -0,0 +1,37 @@ + + + + + + {{section.name}} + + + + + + {{place.icon}} + + {{place.name}} + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss new file mode 100644 index 0000000000..6a0e60c20b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2021 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 '../../../../../../scss/constants'; + +:host { + width: 100%; + height: 100%; +} + +:host ::ng-deep { + .tb-navigation-cards { + .mat-headline { + font-size: 20px; + @media #{$mat-gt-xmd} { + font-size: 24px; + } + } + mat-card { + padding: 0; + margin: 8px; + mat-card-title { + margin: 0; + padding: 24px 16px 16px; + } + mat-card-title+mat-card-content { + padding-top: 0; + } + mat-card-content { + padding: 16px; + } + } + .tb-card-button { + width: 100%; + height: 100%; + max-width: 240px; + &:hover { + border-bottom: none; + } + &:focus { + border-bottom: none; + } + .mat-button-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + mat-icon { + margin: auto; + } + span { + height: 18px; + min-height: 36px; + max-height: 36px; + padding: 0 0 20px 0; + margin: auto; + font-size: 18px; + font-weight: 400; + line-height: 18px; + white-space: normal; + } + } + &.mat-raised-button.mat-primary { + .mat-ripple-element { + opacity: 0.3; + background-color: rgba(255, 255, 255, 0.3); + } + } + } + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts new file mode 100644 index 0000000000..7628c30457 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts @@ -0,0 +1,114 @@ +/// +/// Copyright © 2016-2021 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 { PageComponent } from '@shared/components/page.component'; +import { Component, Input, NgZone, OnInit } from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MenuService } from '@core/services/menu.service'; +import { HomeSection, HomeSectionPlace } from '@core/services/menu.models'; +import { Router } from '@angular/router'; +import { map } from 'rxjs/operators'; + +interface NavigationCardsWidgetSettings { + filterType: 'all' | 'include' | 'exclude'; + filter: string[]; +} + +@Component({ + selector: 'tb-navigation-cards-widget', + templateUrl: './navigation-cards-widget.component.html', + styleUrls: ['./navigation-cards-widget.component.scss'] +}) +export class NavigationCardsWidgetComponent extends PageComponent implements OnInit { + + homeSections$ = this.menuService.homeSections(); + showHomeSections$ = this.homeSections$.pipe( + map((sections) => { + return sections.filter((section) => this.sectionPlaces(section).length > 0); + }) + ); + + cols = null; + + settings: NavigationCardsWidgetSettings; + + @Input() + ctx: WidgetContext; + + constructor(protected store: Store, + private menuService: MenuService, + private ngZone: NgZone, + private router: Router) { + super(store); + } + + ngOnInit(): void { + this.ctx.$scope.navigationCardsWidget = this; + this.settings = this.ctx.settings; + } + + resize() { + this.updateColumnCount(); + } + + private updateColumnCount() { + this.cols = 2; + const width = this.ctx.width; + if (width >= 1280) { + this.cols = 3; + if (width >= 1920) { + this.cols = 4; + } + } + this.ctx.detectChanges(); + } + + navigate($event: Event, path: string) { + $event.preventDefault(); + this.ngZone.run(() => { + this.router.navigateByUrl(path); + }); + } + + sectionPlaces(section: HomeSection): HomeSectionPlace[] { + return section && section.places ? section.places.filter((place) => this.filterPlace(place)) : []; + } + + private filterPlace(place: HomeSectionPlace): boolean { + if (this.settings.filterType === 'include') { + return this.settings.filter.includes(place.path); + } else if (this.settings.filterType === 'exclude') { + return !this.settings.filter.includes(place.path); + } + return true; + } + + sectionColspan(section: HomeSection): number { + if (this.ctx.width >= 960) { + let colspan = this.cols; + const places = this.sectionPlaces(section); + if (places.length <= colspan) { + colspan = places.length; + } + return colspan; + } else { + return 2; + } + } + +} 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 b6ff0b2fbb..0b41d35785 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 @@ -39,72 +39,75 @@ - -
- - - Timestamp - - - - - {{ h.dataKey.label }} - - - - - - - -
- -
-
- - -
+ + Timestamp + + + + + {{ h.dataKey.label }} + + + + + + + +
+ - -
-
-
- - -
- widget.no-data-found -
- - +
+
+ + + + +
+ + + + + + widget.no-data-found +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index e21e29ba65..de4a5628ef 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -40,12 +40,12 @@ import { } from '@shared/models/widget.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; -import {hashCode, isDefined, isDefinedAndNotNull, isNumber} from '@core/utils'; +import { hashCode, isDefined, isNumber, isObject } from '@core/utils'; import cssjs from '@core/css/css'; import { PageLink } from '@shared/models/page/page-link'; import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; -import { BehaviorSubject, fromEvent, merge, Observable, of } from 'rxjs'; +import { BehaviorSubject, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; @@ -129,6 +129,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI public showTimestamp = true; private dateFormatFilter: string; + private subscriptions: Subscription[] = []; + private searchAction: WidgetAction = { name: 'action.search', show: true, @@ -166,40 +168,27 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI debounceTime(150), distinctUntilChanged(), tap(() => { - if (this.displayPagination) { - this.paginators.forEach((paginator) => { - paginator.pageIndex = 0; - }); - } this.sources.forEach((source) => { source.pageLink.textSearch = this.textSearch; + if (this.displayPagination) { + source.pageLink.page = 0; + } }); - this.updateAllData(); + this.loadCurrentSourceRow(); + this.ctx.detectChanges(); }) ) .subscribe(); - if (this.displayPagination) { - this.sorts.forEach((sort, index) => { - sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); - }); - } - this.sorts.forEach((sort, index) => { - const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; - sort.sortChange.subscribe(() => this.paginators.toArray()[index].pageIndex = 0); - ((this.displayPagination ? merge(sort.sortChange, paginator.page) : sort.sortChange) as Observable) - .pipe( - tap(() => this.updateData(sort, paginator, index)) - ) - .subscribe(); + this.sorts.changes.subscribe(() => { + this.initSubscriptionsToSortAndPaginator(); }); - this.updateAllData(); + + this.initSubscriptionsToSortAndPaginator(); } public onDataUpdated() { - this.sources.forEach((source) => { - source.timeseriesDatasource.dataUpdated(this.data); - }); + this.updateCurrentSourceData(); } private initialize() { @@ -210,7 +199,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false; this.showTimestamp = this.settings.showTimestamp !== false; - this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss'; + this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.SSS'; const pageSize = this.settings.defaultPageSize; if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { @@ -305,7 +294,27 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.ctx.activeEntityInfo = activeEntityInfo; } + private initSubscriptionsToSortAndPaginator() { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + this.sorts.forEach((sort, index) => { + let paginator = null; + const observables = [sort.sortChange]; + if (this.displayPagination) { + paginator = this.paginators.toArray()[index]; + this.subscriptions.push( + sort.sortChange.subscribe(() => paginator.pageIndex = 0) + ); + observables.push(paginator.page); + } + this.updateData(sort, paginator); + this.subscriptions.push(merge(...observables).pipe( + tap(() => this.updateData(sort, paginator)) + ).subscribe()); + }); + } + onSourceIndexChanged() { + this.updateCurrentSourceData(); this.updateActiveEntityInfo(); } @@ -326,30 +335,19 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI exitFilterMode() { this.textSearchMode = false; this.textSearch = null; - this.sources.forEach((source, index) => { + this.sources.forEach((source) => { source.pageLink.textSearch = this.textSearch; - const sort = this.sorts.toArray()[index]; - let paginator = null; if (this.displayPagination) { - paginator = this.paginators.toArray()[index]; - paginator.pageIndex = 0; + source.pageLink.page = 0; } - this.updateData(sort, paginator, index); }); + this.loadCurrentSourceRow(); this.ctx.hideTitlePanel = false; this.ctx.detectChanges(true); } - private updateAllData() { - this.sources.forEach((source, index) => { - const sort = this.sorts.toArray()[index]; - const paginator = this.displayPagination ? this.paginators.toArray()[index] : null; - this.updateData(sort, paginator, index); - }); - } - - private updateData(sort: MatSort, paginator: MatPaginator, index: number) { - const source = this.sources[index]; + private updateData(sort: MatSort, paginator: MatPaginator) { + const source = this.sources[this.sourceIndex]; if (this.displayPagination) { source.pageLink.page = paginator.pageIndex; source.pageLink.pageSize = paginator.pageSize; @@ -385,8 +383,16 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { try { style = styleInfo.cellStyleFunction(value); + if (!isObject(style)) { + throw new TypeError(`${style === null ? 'null' : typeof style} instead of style object`); + } + if (Array.isArray(style)) { + throw new TypeError(`Array instead of style object`); + } } catch (e) { style = {}; + console.warn(`Cell style function for data key '${source.header[index - 1].dataKey.label}' in widget ` + + `'${this.ctx.widgetConfig.title}' returns '${e}'. Please check your cell style function.`); } } } @@ -418,7 +424,6 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI if (!isDefined(content)) { return ''; - } else { switch (typeof content) { case 'string': @@ -462,6 +467,18 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } this.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row, entityLabel); } + + public isActiveTab(index: number): boolean { + return index === this.sourceIndex; + } + + private updateCurrentSourceData() { + this.sources[this.sourceIndex].timeseriesDatasource.dataUpdated(this.data); + } + + private loadCurrentSourceRow() { + this.sources[this.sourceIndex].timeseriesDatasource.loadRows(); + } } class TimeseriesDatasource implements DataSource { @@ -482,6 +499,10 @@ class TimeseriesDatasource implements DataSource { } connect(collectionViewer: CollectionViewer): Observable> { + if (this.rowsSubject.isStopped) { + this.rowsSubject.isStopped = false; + this.pageDataSubject.isStopped = false; + } return this.rowsSubject.asObservable(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts index 9372e44655..2581da88ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts @@ -112,6 +112,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']); + this.settings.colorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']); this.normalizationStep = this.settings.normalizationStep; const subscription = this.ctx.defaultSubscription; subscription.callbacks.onDataUpdated = () => { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 2b5c001628..5166a845f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -485,6 +485,9 @@ export class WidgetComponentService { if (isUndefined(result.typeParameters.warnOnPageDataOverflow)) { result.typeParameters.warnOnPageDataOverflow = true; } + if (isUndefined(result.typeParameters.ignoreDataUpdateOnIntervalTick)) { + result.typeParameters.ignoreDataUpdateOnIntervalTick = false; + } if (isUndefined(result.typeParameters.dataKeysOptional)) { result.typeParameters.dataKeysOptional = false; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 0c143f2b57..2d080bc8e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; +import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; +import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; @NgModule({ declarations: @@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor MultipleInputWidgetComponent, TripAnimationComponent, PhotoCameraInputWidgetComponent, - GatewayFormComponent + GatewayFormComponent, + NavigationCardsWidgetComponent, + NavigationCardWidgetComponent ], imports: [ CommonModule, @@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor MultipleInputWidgetComponent, TripAnimationComponent, PhotoCameraInputWidgetComponent, - GatewayFormComponent + GatewayFormComponent, + NavigationCardsWidgetComponent, + NavigationCardWidgetComponent ], providers: [ CustomDialogService, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index e47d96bebe..8b745c480f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -100,87 +100,115 @@
- +
- widget-config.datasource-type + widget-config.datasource-type widget-config.datasource-parameters
-
- {{$index + 1}}. -
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - - + + + + +
+
+ + {{$index + 1}}. +
+
+
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + - - -
- - - - +
+ + + + + + + + + + + + + + + +
- -
- - -
- -
-
+ + +
+ +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 39182661f1..6e1e1cabdf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -23,10 +23,34 @@ .tb-advanced-widget-config { height: 100%; } + .tb-datasources { + + .handle { + cursor: move; + } + + .mat-list { + min-height: 68px; + padding-left: 0; + } + + .mat-list-item { + height: auto; + min-height: 68px; + display: block; + &.dndDraggingSource { + display: none; + } + &.dndPlaceholder { + display: block; + background-color: #ddd; + } + } + } .tb-datasource-type { - min-width: 110px; + min-width: 120px; @media #{$mat-gt-sm} { - max-width: 110px; + max-width: 120px; } } .tb-datasource { @@ -70,13 +94,18 @@ white-space: normal; } .mat-expansion-panel { - &.tb-datasources{ + &.tb-datasources { &.mat-expanded { overflow: visible; } .mat-expansion-panel-body{ padding: 0 12px 16px; } + .mat-list-base .mat-list-item { + .mat-list-item-content { + padding: 0; + } + } } .mat-expansion-panel-content { font: inherit; @@ -110,6 +139,11 @@ } } } + .tb-datasource-name.no-border-top { + .mat-form-field-infix { + border-top: 0; + } + } } .tb-data-keys { @media #{$mat-gt-sm} { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 05c6264a92..523913afe4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -40,7 +40,7 @@ import { Validators } from '@angular/forms'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { deepClone, isDefined, isObject } from '@app/core/utils'; +import { deepClone, isDefined, isObject, isUndefined } from '@app/core/utils'; import { alarmFields, AlarmSearchStatus, @@ -71,6 +71,7 @@ import { Filter, Filters } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; +import { DndDropEvent } from 'ngx-drag-drop'; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -141,7 +142,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont widgetType: widgetType; datasourceType = DatasourceType; - datasourceTypes: Array; + datasourceTypes: Array = []; datasourceTypesTranslations = datasourceTypeTranslationMap; widgetConfigCallbacks: WidgetConfigCallbacks = { @@ -186,11 +187,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } ngOnInit(): void { - if (this.functionsOnly) { - this.datasourceTypes = [DatasourceType.function]; - } else { - this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; - } this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); @@ -295,6 +291,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } private buildForms() { + if (this.functionsOnly) { + this.datasourceTypes = [DatasourceType.function]; + } else { + this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; + if (this.widgetType === widgetType.latest) { + this.datasourceTypes.push(DatasourceType.entityCount); + } + } this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); @@ -517,22 +521,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } private buildDatasourceForm(datasource?: Datasource): FormGroup { - const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; + let dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional + || datasource?.type === DatasourceType.entityCount; const datasourceFormGroup = this.fb.group( { type: [datasource ? datasource.type : null, [Validators.required]], name: [datasource ? datasource.name : null, []], entityAliasId: [datasource ? datasource.entityAliasId : null, - datasource && datasource.type === DatasourceType.entity ? [Validators.required] : []], + datasource && (datasource.type === DatasourceType.entity || + datasource.type === DatasourceType.entityCount) ? [Validators.required] : []], filterId: [datasource ? datasource.filterId : null, []], dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []] } ); datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => { datasourceFormGroup.get('entityAliasId').setValidators( - type === DatasourceType.entity ? [Validators.required] : [] + (type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : [] ); + dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional + || type === DatasourceType.entityCount; + datasourceFormGroup.get('dataKeys').setValidators(dataKeysRequired ? [Validators.required] : []); datasourceFormGroup.get('entityAliasId').updateValueAndValidity(); + datasourceFormGroup.get('dataKeys').updateValueAndValidity(); }); return datasourceFormGroup; } @@ -659,8 +669,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema; } + public dndDatasourceMoved(index: number) { + this.datasourcesFormArray().removeAt(index); + } + + public onDatasourceDrop(event: DndDropEvent) { + let index = event.index; + if (isUndefined(index)) { + index = this.datasourcesFormArray().length; + } + this.datasourcesFormArray().insert(index, + this.buildDatasourceForm(event.data) + ); + } + public removeDatasource(index: number) { - (this.dataSettings.get('datasources') as FormArray).removeAt(index); + this.datasourcesFormArray().removeAt(index); } public addDatasource() { @@ -673,8 +697,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont dataKeys: [] }; } - const datasourcesFormArray = this.dataSettings.get('datasources') as FormArray; - datasourcesFormArray.push(this.buildDatasourceForm(newDatasource)); + this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource)); } public generateDataKey(chip: any, type: DataKeyType): DataKey { @@ -704,6 +727,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont if (!result.funcBody) { result.funcBody = 'return prevValue + 1;'; } + } else if (type === DataKeyType.count) { + result.name = 'count'; } if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) { result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index ef5555e5eb..aa47b97c20 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -22,6 +22,7 @@ import { ComponentFactoryResolver, ComponentRef, ElementRef, + Inject, Injector, Input, NgZone, @@ -54,7 +55,7 @@ import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; -import { deepClone, isDefined, objToBase64URI } from '@core/utils'; +import { deepClone, insertVariable, isDefined, objToBase64, objToBase64URI } from '@core/utils'; import { IDynamicWidgetComponent, WidgetContext, @@ -93,6 +94,9 @@ import { EntityDataService } from '@core/api/entity-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationType } from '@core/notification/notification.models'; import { AlarmDataService } from '@core/api/alarm-data.service'; +import { MatDialog } from '@angular/material/dialog'; +import { ComponentType } from '@angular/cdk/portal'; +import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; @Component({ selector: 'tb-widget', @@ -161,6 +165,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private componentFactoryResolver: ComponentFactoryResolver, private elementRef: ElementRef, private injector: Injector, + private dialog: MatDialog, + @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType, private widgetService: WidgetService, private resources: ResourcesService, private timeService: TimeService, @@ -889,6 +895,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI hasDataPageLink: this.typeParameters.hasDataPageLink, singleEntity: this.typeParameters.singleEntity, warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow, + ignoreDataUpdateOnIntervalTick: this.typeParameters.ignoreDataUpdateOnIntervalTick, comparisonEnabled: comparisonSettings.comparisonEnabled, timeForComparison: comparisonSettings.timeForComparison }; @@ -1007,7 +1014,11 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI const params = deepClone(this.widgetContext.stateController.getStateParams()); this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); if (type === WidgetActionType.openDashboardState) { - this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); + if (descriptor.openInSeparateDialog) { + this.openDashboardStateInDialog(descriptor, entityId, entityName, additionalParams, entityLabel); + } else { + this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); + } } else { this.widgetContext.stateController.updateState(targetDashboardStateId, params, descriptor.openRightLayout); } @@ -1083,6 +1094,61 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI } } + private openDashboardStateInDialog(descriptor: WidgetActionDescriptor, + entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { + const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); + const stateObject: StateObject = {}; + stateObject.params = {}; + const targetEntityParamName = descriptor.stateEntityParamName; + const targetDashboardStateId = descriptor.targetDashboardStateId; + let targetEntityId: EntityId; + if (descriptor.setEntityId) { + targetEntityId = entityId; + } + this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); + if (targetDashboardStateId) { + stateObject.id = targetDashboardStateId; + } + let title = descriptor.dialogTitle; + if (!title) { + if (targetDashboardStateId && dashboard.configuration.states) { + const dashboardState = dashboard.configuration.states[targetDashboardStateId]; + if (dashboardState) { + title = dashboardState.name; + } + } + } + if (!title) { + title = dashboard.title; + } + title = this.utils.customTranslation(title, title); + const params = stateObject.params; + const paramsEntityName = params && params.entityName ? params.entityName : ''; + const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : ''; + title = insertVariable(title, 'entityName', paramsEntityName); + title = insertVariable(title, 'entityLabel', paramsEntityLabel); + for (const prop of Object.keys(params)) { + if (params[prop] && params[prop].entityName) { + title = insertVariable(title, prop + ':entityName', params[prop].entityName); + } + if (params[prop] && params[prop].entityLabel) { + title = insertVariable(title, prop + ':entityLabel', params[prop].entityLabel); + } + } + this.dialog.open(this.embedDashboardDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dashboard, + state: objToBase64([ stateObject ]), + title, + hideToolbar: descriptor.dialogHideDashboardToolbar, + width: descriptor.dialogWidth, + height: descriptor.dialogHeight + } + }); + } + private elementClick($event: Event) { const e = ($event.target || $event.srcElement) as Element; if (e.id) { diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index fbebf66d4c..245cb6503b 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -108,9 +108,15 @@
- - {{ 'device.is-gateway' | translate }} - +
+ + {{ 'device.is-gateway' | translate }} + + + {{ 'device.overwrite-activity-time' | translate }} + +
device.description diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 66f115af93..1cba31aa24 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -39,8 +39,8 @@ import { BaseData, HasId } from '@shared/models/base-data'; import { EntityType } from '@shared/models/entity-type.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { EntityId } from '@shared/models/id/entity-id'; -import { Observable, of, Subscription } from 'rxjs'; -import { map, mergeMap, tap } from 'rxjs/operators'; +import { Observable, of, Subscription, throwError } from 'rxjs'; +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { DeviceService } from '@core/http/device.service'; import { ErrorStateMatcher } from '@angular/material/core'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; @@ -108,6 +108,7 @@ export class DeviceWizardDialogComponent extends name: ['', Validators.required], label: [''], gateway: [false], + overwriteActivityTime: [false], transportType: [DeviceTransportType.DEFAULT, Validators.required], addProfileType: [0], deviceProfileId: [null, Validators.required], @@ -314,6 +315,7 @@ export class DeviceWizardDialogComponent extends deviceProfileId: profileId, additionalInfo: { gateway: this.deviceWizardFormGroup.get('gateway').value, + overwriteActivityTime: this.deviceWizardFormGroup.get('overwriteActivityTime').value, description: this.deviceWizardFormGroup.get('description').value }, customerId: null @@ -333,7 +335,15 @@ export class DeviceWizardDialogComponent extends mergeMap( (deviceCredentials) => { const deviceCredentialsValue = {...deviceCredentials, ...this.credentialsFormGroup.value.credential}; - return this.deviceService.saveDeviceCredentials(deviceCredentialsValue); + return this.deviceService.saveDeviceCredentials(deviceCredentialsValue).pipe( + catchError(e => { + return this.deviceService.deleteDevice(device.id.id).pipe( + mergeMap(() => { + return throwError(e); + } + )); + }) + ); } ), map(() => true)); diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 42f67b11bb..e1e3675e89 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -30,6 +30,7 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; import { ActivatedRoute } from '@angular/router'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { DAY, historyInterval } from '@shared/models/time/time.models'; export type EntityBooleanFunction> = (entity: T) => boolean; export type EntityStringFunction> = (entity: T) => string; @@ -135,6 +136,7 @@ export class EntityTableConfig, P extends PageLink = P onLoadAction: (route: ActivatedRoute) => void = null; table: EntitiesTableComponent = null; useTimePageLink = false; + defaultTimewindowInterval = historyInterval(DAY); entityType: EntityType = null; tableTitle = ''; selectionEnabled = true; @@ -162,7 +164,7 @@ export class EntityTableConfig, P extends PageLink = P dataSource: (dataLoadedFunction: (col?: number, row?: number) => void) => EntitiesDataSource = (dataLoadedFunction: (col?: number, row?: number) => void) => { return new EntitiesDataSource(this.entitiesFetchFunction, this.entitySelectionEnabled, dataLoadedFunction); - }; + } detailsReadonly: EntityBooleanFunction = () => false; entitySelectionEnabled: EntityBooleanFunction = () => true; deleteEnabled: EntityBooleanFunction = () => true; diff --git a/ui-ngx/src/app/modules/home/models/services.map.ts b/ui-ngx/src/app/modules/home/models/services.map.ts index 9e33301ed7..64e94f24df 100644 --- a/ui-ngx/src/app/modules/home/models/services.map.ts +++ b/ui-ngx/src/app/modules/home/models/services.map.ts @@ -32,7 +32,8 @@ import { DashboardService } from '@core/http/dashboard.service'; import { UserService } from '@core/http/user.service'; import { AlarmService } from '@core/http/alarm.service'; import { Router } from '@angular/router'; -import { BroadcastService } from "@core/services/broadcast.service"; +import { BroadcastService } from '@core/services/broadcast.service'; +import { ImportExportService } from '@home/components/import-export/import-export.service'; export const ServicesMap = new Map>( [ @@ -53,6 +54,7 @@ export const ServicesMap = new Map>( ['utils', UtilsService], ['translate', TranslateService], ['http', HttpClient], - ['router', Router] + ['router', Router], + ['importExport', ImportExportService] ] ); diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index 1df4aed59e..1928a1f404 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { OAuth2Service } from '@core/http/oauth2.service'; import { UserProfileResolver } from '@home/pages/profile/profile-routing.module'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -48,7 +49,7 @@ const routes: Routes = [ { path: 'settings', data: { - auth: [Authority.SYS_ADMIN], + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], breadcrumb: { label: 'admin.system-settings', icon: 'settings' @@ -57,8 +58,13 @@ const routes: Routes = [ children: [ { path: '', - redirectTo: 'general', - pathMatch: 'full' + data: { + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], + redirectTo: { + SYS_ADMIN: '/settings/general', + TENANT_ADMIN: '/settings/home' + } + } }, { path: 'general', @@ -127,6 +133,19 @@ const routes: Routes = [ resolve: { loginProcessingUrl: OAuth2LoginProcessingUrlResolver } + }, + { + path: 'home', + component: HomeSettingsComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.TENANT_ADMIN], + title: 'admin.home-settings', + breadcrumb: { + label: 'admin.home-settings', + icon: 'settings_applications' + } + } } ] } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index a170ce10b4..1671220cfe 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -26,6 +26,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; +import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; @NgModule({ declarations: @@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial SmsProviderComponent, SendTestSmsDialogComponent, SecuritySettingsComponent, - OAuth2SettingsComponent + OAuth2SettingsComponent, + HomeSettingsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html new file mode 100644 index 0000000000..0191724a24 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html @@ -0,0 +1,54 @@ + +
+ + +
+ admin.home-settings +
+
+ + +
+ +
+
+
+
+ + + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} + +
+
+
+ +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.scss new file mode 100644 index 0000000000..482889cf77 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 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 "../../../../../scss/constants"; + +:host { + .tb-default-dashboard { + tb-dashboard-autocomplete { + @media #{$mat-gt-sm} { + padding-right: 12px; + } + + @media #{$mat-lt-md} { + padding-bottom: 12px; + } + } + mat-checkbox { + @media #{$mat-gt-sm} { + margin-top: 16px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts new file mode 100644 index 0000000000..a92579d701 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts @@ -0,0 +1,84 @@ +/// +/// Copyright © 2016-2021 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, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { PageComponent } from '@shared/components/page.component'; +import { Router } from '@angular/router'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; +import { DashboardService } from '@core/http/dashboard.service'; +import { HomeDashboardInfo } from '@shared/models/dashboard.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { DashboardId } from '@shared/models/id/dashboard-id'; + +@Component({ + selector: 'tb-home-settings', + templateUrl: './home-settings.component.html', + styleUrls: ['./home-settings.component.scss', './settings-card.scss'] +}) +export class HomeSettingsComponent extends PageComponent implements OnInit, HasConfirmForm { + + homeSettings: FormGroup; + + constructor(protected store: Store, + private router: Router, + private dashboardService: DashboardService, + public fb: FormBuilder) { + super(store); + } + + ngOnInit() { + this.homeSettings = this.fb.group({ + dashboardId: [null], + hideDashboardToolbar: [true] + }); + this.dashboardService.getTenantHomeDashboardInfo().subscribe( + (homeDashboardInfo) => { + this.setHomeDashboardInfo(homeDashboardInfo); + } + ); + } + + save(): void { + const strDashboardId = this.homeSettings.get('dashboardId').value; + const dashboardId: DashboardId = strDashboardId ? new DashboardId(strDashboardId) : null; + const hideDashboardToolbar = this.homeSettings.get('hideDashboardToolbar').value; + const homeDashboardInfo: HomeDashboardInfo = { + dashboardId, + hideDashboardToolbar + }; + this.dashboardService.setTenantHomeDashboardInfo(homeDashboardInfo).subscribe( + () => { + this.setHomeDashboardInfo(homeDashboardInfo); + } + ); + } + + confirmForm(): FormGroup { + return this.homeSettings; + } + + private setHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo) { + this.homeSettings.reset({ + dashboardId: homeDashboardInfo?.dashboardId?.id, + hideDashboardToolbar: isDefinedAndNotNull(homeDashboardInfo?.hideDashboardToolbar) ? + homeDashboardInfo?.hideDashboardToolbar : true + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts index a95b396d92..47bf4bd0d2 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts @@ -19,7 +19,6 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@app/shared/shared.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { ApiUsageComponent } from '@home/pages/api-usage/api-usage.component'; -import { DashboardModule } from '@home/pages/dashboard/dashboard.module'; import { ApiUsageRoutingModule } from '@home/pages/api-usage/api-usage-routing.module'; @NgModule({ @@ -31,7 +30,6 @@ import { ApiUsageRoutingModule } from '@home/pages/api-usage/api-usage-routing.m CommonModule, SharedModule, HomeComponentsModule, - DashboardModule, ApiUsageRoutingModule ] }) diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts index 166efa46e2..2807baae04 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -24,7 +24,7 @@ import { CustomersTableConfigResolver } from './customers-table-config.resolver' import { DevicesTableConfigResolver } from '@modules/home/pages/device/devices-table-config.resolver'; import { AssetsTableConfigResolver } from '../asset/assets-table-config.resolver'; import { DashboardsTableConfigResolver } from '@modules/home/pages/dashboard/dashboards-table-config.resolver'; -import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { dashboardBreadcumbLabelFunction, DashboardResolver } from '@home/pages/dashboard/dashboard-routing.module'; diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html index fce5c1926c..392ae7b6c4 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html @@ -72,6 +72,21 @@ customer.description
+
+
+ + + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.scss b/ui-ngx/src/app/modules/home/pages/customer/customer.component.scss new file mode 100644 index 0000000000..3a4ec550cd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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 "../../../../../scss/constants"; + +:host { + .tb-default-dashboard { + tb-dashboard-autocomplete { + @media #{$mat-gt-sm} { + padding-right: 12px; + } + + @media #{$mat-lt-md} { + padding-bottom: 12px; + } + } + mat-checkbox { + @media #{$mat-gt-sm} { + margin-top: 16px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts index 809beef5cd..8c609fe2ef 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts @@ -23,10 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti import { TranslateService } from '@ngx-translate/core'; import { ContactBasedComponent } from '../../components/entity/contact-based.component'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-customer', - templateUrl: './customer.component.html' + templateUrl: './customer.component.html', + styleUrls: ['./customer.component.scss'] }) export class CustomerComponent extends ContactBasedComponent { @@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent { title: [entity ? entity.title : '', [Validators.required]], additionalInfo: this.fb.group( { - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null], + homeDashboardHideToolbar: [entity && entity.additionalInfo && + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true] } ) } @@ -65,6 +70,11 @@ export class CustomerComponent extends ContactBasedComponent { this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic; this.entityForm.patchValue({title: entity.title}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + this.entityForm.patchValue({additionalInfo: + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}}); + this.entityForm.patchValue({additionalInfo: + {homeDashboardHideToolbar: entity.additionalInfo && + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}}); } onCustomerIdCopied(event) { diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts index 7cf02aea32..55c1fc0d28 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts @@ -20,7 +20,7 @@ import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/ import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; import { Authority } from '@shared/models/authority.enum'; import { DashboardsTableConfigResolver } from './dashboards-table-config.resolver'; -import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; +import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { Observable } from 'rxjs'; import { Dashboard } from '@app/shared/models/dashboard.models'; diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts index 9b4e8690d7..0c1e7887f0 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts @@ -24,44 +24,19 @@ import { DashboardRoutingModule } from './dashboard-routing.module'; import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; -import { DashboardPageComponent } from '@home/pages/dashboard/dashboard-page.component'; -import { DashboardToolbarComponent } from './dashboard-toolbar.component'; -import { StatesControllerModule } from '@home/pages/dashboard/states/states-controller.module'; -import { DashboardLayoutComponent } from './layout/dashboard-layout.component'; -import { EditWidgetComponent } from './edit-widget.component'; -import { DashboardWidgetSelectComponent } from './dashboard-widget-select.component'; -import { AddWidgetDialogComponent } from './add-widget-dialog.component'; -import { ManageDashboardLayoutsDialogComponent } from './layout/manage-dashboard-layouts-dialog.component'; -import { DashboardSettingsDialogComponent } from './dashboard-settings-dialog.component'; -import { ManageDashboardStatesDialogComponent } from './states/manage-dashboard-states-dialog.component'; -import { DashboardStateDialogComponent } from './states/dashboard-state-dialog.component'; @NgModule({ declarations: [ DashboardFormComponent, DashboardTabsComponent, ManageDashboardCustomersDialogComponent, - MakeDashboardPublicDialogComponent, - DashboardToolbarComponent, - DashboardPageComponent, - DashboardLayoutComponent, - EditWidgetComponent, - DashboardWidgetSelectComponent, - AddWidgetDialogComponent, - ManageDashboardLayoutsDialogComponent, - DashboardSettingsDialogComponent, - ManageDashboardStatesDialogComponent, - DashboardStateDialogComponent - ], - exports: [ - DashboardPageComponent + MakeDashboardPublicDialogComponent ], imports: [ CommonModule, SharedModule, HomeComponentsModule, HomeDialogsModule, - StatesControllerModule, DashboardRoutingModule ] }) diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html index 8022439213..f1a7af1e46 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -42,7 +42,7 @@
diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index 7f7465a5e6..b01aced285 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -100,9 +100,15 @@ required>
- - {{ 'device.is-gateway' | translate }} - +
+ + {{ 'device.is-gateway' | translate }} + + + {{ 'device.overwrite-activity-time' | translate }} + +
device.description diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index 782ed9e23a..261bb1eba5 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -84,6 +84,7 @@ export class DeviceComponent extends EntityComponent { additionalInfo: this.fb.group( { gateway: [entity && entity.additionalInfo ? entity.additionalInfo.gateway : false], + overwriteActivityTime: [entity && entity.additionalInfo ? entity.additionalInfo.overwriteActivityTime : false], description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], } ) @@ -96,8 +97,13 @@ export class DeviceComponent extends EntityComponent { this.entityForm.patchValue({deviceProfileId: entity.deviceProfileId}); this.entityForm.patchValue({label: entity.label}); this.entityForm.patchValue({deviceData: entity.deviceData}); - this.entityForm.patchValue({additionalInfo: - {gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false}}); + this.entityForm.patchValue({ + additionalInfo: + { + gateway: entity.additionalInfo ? entity.additionalInfo.gateway : false, + overwriteActivityTime: entity.additionalInfo ? entity.additionalInfo.overwriteActivityTime : false + } + }); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); } diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts index 9347fab505..152cd717a7 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts @@ -24,6 +24,8 @@ import { DeviceCredentialsDialogComponent } from '@modules/home/pages/device/dev import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; +import { SecurityConfigComponent } from '@home/pages/device/lwm2m/security-config.component'; +import { SecurityConfigServerComponent } from '@home/pages/device/lwm2m/security-config-server.component'; import { DefaultDeviceConfigurationComponent } from './data/default-device-configuration.component'; import { DeviceConfigurationComponent } from './data/device-configuration.component'; import { DeviceDataComponent } from './data/device-data.component'; @@ -46,7 +48,9 @@ import { SnmpDeviceTransportConfigurationComponent } from './data/snmp-device-tr DeviceComponent, DeviceTabsComponent, DeviceTableHeaderComponent, - DeviceCredentialsDialogComponent + DeviceCredentialsDialogComponent, + SecurityConfigComponent, + SecurityConfigServerComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config-server.component.html b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config-server.component.html new file mode 100644 index 0000000000..bd02c9dee3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config-server.component.html @@ -0,0 +1,101 @@ + +
+
+
+
+ + device.lwm2m-security-config.mode + + + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityMode]) }} + + + +
+
+
+
+ + {{ 'device.lwm2m-security-config.client-publicKey-or-id' | translate }} + + {{clientPublicKeyOrId.value?.length || 0}}/{{lenMaxClientPublicKeyOrId}} + + {{ 'device.lwm2m-security-config.client-publicKey-or-id' | translate }} + {{ 'device.lwm2m-security-config.required' | translate }} + + + {{ 'device.lwm2m-security-config.client-key' | translate }} + {{ 'device.lwm2m-security-config.pattern_hex_dec' | translate: { + count: 0 + } }} + + + {{ 'device.lwm2m-security-config.client-key' | translate }} + {{ 'device.lwm2m-security-config.pattern_hex_dec' | translate: { + count: lenMaxClientPublicKeyOrId + } }} + + + + {{ 'device.lwm2m-security-config.client-secret-key' | translate }} + + {{clientSecretKey.value?.length || 0}}/{{lenMaxClientSecretKey}} + + {{ 'device.lwm2m-security-config.client-secret-key' | translate }} + {{ 'device.lwm2m-security-config.required' | translate }} + + + {{ 'device.lwm2m-security-config.client-key' | translate }} + {{ 'device.lwm2m-security-config.pattern_hex_dec' | translate: { + count: 0 + } }} + + + {{ 'device.lwm2m-security-config.client-key' | translate }} + {{ 'device.lwm2m-security-config.pattern_hex_dec' | translate: { + count: lenMaxClientSecretKey + } }} + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config-server.component.ts b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config-server.component.ts new file mode 100644 index 0000000000..a068bb589e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config-server.component.ts @@ -0,0 +1,142 @@ +/// +/// Copyright © 2016-2021 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, Inject, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + DeviceCredentialsDialogLwm2mData, + KEY_REGEXP_HEX_DEC, + LEN_MAX_PRIVATE_KEY, + LEN_MAX_PSK, + LEN_MAX_PUBLIC_KEY_RPK, + LEN_MAX_PUBLIC_KEY_X509, + SECURITY_CONFIG_MODE, + SECURITY_CONFIG_MODE_NAMES, + ServerSecurityConfig +} from '@home/pages/device/lwm2m/security-config.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { PageComponent } from '@shared/components/page.component'; + +@Component({ + selector: 'tb-security-config-server-lwm2m', + templateUrl: './security-config-server.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SecurityConfigServerComponent), + multi: true + } + ] +}) + +export class SecurityConfigServerComponent extends PageComponent implements OnInit, ControlValueAccessor { + + securityConfigLwM2MType = SECURITY_CONFIG_MODE; + securityConfigLwM2MTypes = Object.keys(SECURITY_CONFIG_MODE); + credentialTypeLwM2MNamesMap = SECURITY_CONFIG_MODE_NAMES; + lenMinClientPublicKeyOrId = 0; + lenMaxClientPublicKeyOrId = LEN_MAX_PUBLIC_KEY_RPK; + lenMinClientSecretKey = LEN_MAX_PRIVATE_KEY; + lenMaxClientSecretKey = LEN_MAX_PRIVATE_KEY; + + @Input() serverFormGroup: FormGroup; + + constructor(protected store: Store, + @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogLwm2mData, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store); + } + + ngOnInit(): void { + this.registerDisableOnLoadFormControl(this.serverFormGroup.get('securityMode')); + } + + private updateValueFields(serverData: ServerSecurityConfig): void { + this.serverFormGroup.patchValue(serverData, {emitEvent: false}); + const securityMode = this.serverFormGroup.get('securityMode').value as SECURITY_CONFIG_MODE; + this.updateValidate(securityMode); + } + + private updateValidate(securityMode: SECURITY_CONFIG_MODE): void { + switch (securityMode) { + case SECURITY_CONFIG_MODE.NO_SEC: + this.serverFormGroup.get('clientPublicKeyOrId').setValidators([]); + this.serverFormGroup.get('clientSecretKey').setValidators([]); + break; + case SECURITY_CONFIG_MODE.PSK: + this.lenMinClientPublicKeyOrId = 0; + this.lenMaxClientPublicKeyOrId = LEN_MAX_PUBLIC_KEY_RPK; + this.lenMinClientSecretKey = LEN_MAX_PSK; + this.lenMaxClientSecretKey = LEN_MAX_PSK; + this.setValidatorsSecurity(securityMode); + break; + case SECURITY_CONFIG_MODE.RPK: + this.lenMinClientPublicKeyOrId = LEN_MAX_PUBLIC_KEY_RPK; + this.lenMaxClientPublicKeyOrId = LEN_MAX_PUBLIC_KEY_RPK; + this.lenMinClientSecretKey = LEN_MAX_PRIVATE_KEY; + this.lenMaxClientSecretKey = LEN_MAX_PRIVATE_KEY; + this.setValidatorsSecurity(securityMode); + break; + case SECURITY_CONFIG_MODE.X509: + this.lenMinClientPublicKeyOrId = 0; + this.lenMaxClientPublicKeyOrId = LEN_MAX_PUBLIC_KEY_X509; + this.lenMinClientSecretKey = LEN_MAX_PRIVATE_KEY; + this.lenMaxClientSecretKey = LEN_MAX_PRIVATE_KEY; + this.setValidatorsSecurity(securityMode); + break; + } + this.serverFormGroup.updateValueAndValidity(); + } + + private setValidatorsSecurity = (securityMode: SECURITY_CONFIG_MODE): void => { + if (securityMode === SECURITY_CONFIG_MODE.PSK) { + this.serverFormGroup.get('clientPublicKeyOrId').setValidators([Validators.required]); + } else { + this.serverFormGroup.get('clientPublicKeyOrId').setValidators([Validators.required, + Validators.pattern(KEY_REGEXP_HEX_DEC), + Validators.minLength(this.lenMinClientPublicKeyOrId), + Validators.maxLength(this.lenMaxClientPublicKeyOrId)]); + } + + this.serverFormGroup.get('clientSecretKey').setValidators([Validators.required, + Validators.pattern(KEY_REGEXP_HEX_DEC), + Validators.minLength(this.lenMinClientSecretKey), + Validators.maxLength(this.lenMaxClientSecretKey)]); + } + + securityModeChanged(securityMode: SECURITY_CONFIG_MODE): void { + this.updateValidate(securityMode); + } + + writeValue(value: any): void { + if (value) { + this.updateValueFields(value); + } + } + + registerOnChange(fn: (value: any) => any): void { + } + + registerOnTouched(fn: any): void { + } + + setDisabledState?(isDisabled: boolean): void { + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.component.html b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.component.html new file mode 100644 index 0000000000..db0946b371 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.component.html @@ -0,0 +1,180 @@ + +
+ +

{{ title }}

+ + +
+
+
+ + device.lwm2m-security-config.endpoint + + + {{ 'device.lwm2m-security-config.endpoint' | translate }} + {{ 'device.lwm2m-security-config.required' | translate }} + + + + + +
+ + device.lwm2m-security-config.mode + + + {{ credentialTypeLwM2MNamesMap.get(securityConfigLwM2MType[securityConfigClientMode]) }} + + + +
+ + {{ 'device.lwm2m-security-config.identity' | translate }} + + + {{ 'device.lwm2m-security-config.identity' | translate }} + {{ 'device.lwm2m-security-config.required' | translate }} + + +
+
+ + {{ 'device.lwm2m-security-config.client-key' | translate }} + + {{clientKey.value?.length || 0}}/{{lenMaxKeyClient}} + + {{ 'device.lwm2m-security-config.client-key' | translate }} + {{ 'device.lwm2m-security-config.required' | translate }} + + + {{ 'device.lwm2m-security-config.client-key' | translate }} + {{ 'device.lwm2m-security-config.pattern_hex_dec' | translate: { + count: 0 + } }} + + + {{ 'device.lwm2m-security-config.client-key' | translate }} + {{ 'device.lwm2m-security-config.pattern_hex_dec' | translate: { + count: lenMaxKeyClient + } }} + + +
+
+ + {{ 'device.lwm2m-security-config.client-certificate' | translate }} + +
+
+
+
+ + +
+ + + + +
{{ 'device.lwm2m-security-config.bootstrap-server' | translate | uppercase }}
+
+
+ +
+ + +
+
+
+
+ + + + +
{{ 'device.lwm2m-security-config.lwm2m-server' | translate | uppercase }}
+
+
+ +
+ + +
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.component.ts b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.component.ts new file mode 100644 index 0000000000..9d1116e06f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.component.ts @@ -0,0 +1,385 @@ +/// +/// Copyright © 2016-2021 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, OnInit } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { + BOOTSTRAP_SERVER, + BOOTSTRAP_SERVERS, + ClientSecurityConfigNoSEC, + ClientSecurityConfigPSK, + ClientSecurityConfigRPK, + ClientSecurityConfigX509, + DeviceCredentialsDialogLwm2mData, + getDefaultClientSecurityConfigType, + JSON_ALL_CONFIG, + KEY_REGEXP_HEX_DEC, + LEN_MAX_PSK, + LEN_MAX_PUBLIC_KEY_RPK, + LWM2M_SERVER, + SECURITY_CONFIG_MODE, + SECURITY_CONFIG_MODE_NAMES, + SecurityConfigModels +} from './security-config.models'; +import { WINDOW } from '@core/services/window.service'; +import { MatTabChangeEvent } from '@angular/material/tabs'; +import { MatTab } from '@angular/material/tabs/tab'; + +@Component({ + selector: 'tb-security-config-lwm2m', + templateUrl: './security-config.component.html', + styleUrls: [] +}) + +export class SecurityConfigComponent extends DialogComponent implements OnInit { + + lwm2mConfigFormGroup: FormGroup; + title: string; + submitted = false; + securityConfigLwM2MType = SECURITY_CONFIG_MODE; + securityConfigLwM2MTypes = Object.keys(SECURITY_CONFIG_MODE); + credentialTypeLwM2MNamesMap = SECURITY_CONFIG_MODE_NAMES; + formControlNameJsonAllConfig = JSON_ALL_CONFIG; + jsonAllConfig: SecurityConfigModels; + bootstrapServers: string; + bootstrapServer: string; + lwm2mServer: string; + jsonObserveData: {}; + lenMaxKeyClient = LEN_MAX_PSK; + tabPrevious: MatTab; + tabIndexPrevious = 0; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogLwm2mData, + public dialogRef: MatDialogRef, + public fb: FormBuilder, + public translate: TranslateService, + @Inject(WINDOW) private window: Window) { + super(store, router, dialogRef); + } + + ngOnInit(): void { + this.jsonAllConfig = JSON.parse(JSON.stringify(this.data.jsonAllConfig)) as SecurityConfigModels; + this.initConstants(); + this.lwm2mConfigFormGroup = this.initLwm2mConfigFormGroup(); + this.title = this.translate.instant('device.lwm2m-security-info') + ': ' + this.data.endPoint; + this.lwm2mConfigFormGroup.get('clientCertificate').disable(); + this.initClientSecurityConfig(this.lwm2mConfigFormGroup.get('jsonAllConfig').value); + this.registerDisableOnLoadFormControl(this.lwm2mConfigFormGroup.get('securityConfigClientMode')); + } + + private initConstants = (): void => { + this.bootstrapServers = BOOTSTRAP_SERVERS; + this.bootstrapServer = BOOTSTRAP_SERVER; + this.lwm2mServer = LWM2M_SERVER; + } + + /** + * initChildesFormGroup + */ + get bootstrapFormGroup(): FormGroup { + return this.lwm2mConfigFormGroup.get('bootstrapFormGroup') as FormGroup; + } + + get lwm2mServerFormGroup(): FormGroup { + return this.lwm2mConfigFormGroup.get('lwm2mServerFormGroup') as FormGroup; + } + + get observeAttrFormGroup(): FormGroup { + return this.lwm2mConfigFormGroup.get('observeFormGroup') as FormGroup; + } + + private initClientSecurityConfig = (jsonAllConfig: SecurityConfigModels): void => { + switch (jsonAllConfig.client.securityConfigClientMode) { + case SECURITY_CONFIG_MODE.NO_SEC: + break; + case SECURITY_CONFIG_MODE.PSK: + const clientSecurityConfigPSK = jsonAllConfig.client as ClientSecurityConfigPSK; + this.lwm2mConfigFormGroup.patchValue({ + identityPSK: clientSecurityConfigPSK.identity, + clientKey: clientSecurityConfigPSK.key, + }, {emitEvent: false}); + break; + case SECURITY_CONFIG_MODE.RPK: + const clientSecurityConfigRPK = jsonAllConfig.client as ClientSecurityConfigRPK; + this.lwm2mConfigFormGroup.patchValue({ + clientKey: clientSecurityConfigRPK.key, + }, {emitEvent: false}); + break; + case SECURITY_CONFIG_MODE.X509: + const clientSecurityConfigX509 = jsonAllConfig.client as ClientSecurityConfigX509; + this.lwm2mConfigFormGroup.patchValue({ + clientCertificate: clientSecurityConfigX509.x509 + }, {emitEvent: false}); + break; + } + this.securityConfigClientUpdateValidators(this.lwm2mConfigFormGroup.get('securityConfigClientMode').value); + } + + securityConfigClientModeChanged = (mode: SECURITY_CONFIG_MODE): void => { + switch (mode) { + case SECURITY_CONFIG_MODE.NO_SEC: + const clientSecurityConfigNoSEC = getDefaultClientSecurityConfigType(mode) as ClientSecurityConfigNoSEC; + this.jsonAllConfig.client = clientSecurityConfigNoSEC; + this.lwm2mConfigFormGroup.patchValue({ + jsonAllConfig: this.jsonAllConfig, + clientCertificate: false + }, {emitEvent: false}); + break; + case SECURITY_CONFIG_MODE.PSK: + const clientSecurityConfigPSK = getDefaultClientSecurityConfigType(mode, this.lwm2mConfigFormGroup.get('endPoint') + .value) as ClientSecurityConfigPSK; + clientSecurityConfigPSK.identity = this.data.endPoint; + clientSecurityConfigPSK.key = this.lwm2mConfigFormGroup.get('clientKey').value; + this.jsonAllConfig.client = clientSecurityConfigPSK; + this.lwm2mConfigFormGroup.patchValue({ + identityPSK: clientSecurityConfigPSK.identity, + clientCertificate: false + }, {emitEvent: false}); + break; + case SECURITY_CONFIG_MODE.RPK: + const clientSecurityConfigRPK = getDefaultClientSecurityConfigType(mode) as ClientSecurityConfigRPK; + clientSecurityConfigRPK.key = this.lwm2mConfigFormGroup.get('clientKey').value; + this.jsonAllConfig.client = clientSecurityConfigRPK; + this.lwm2mConfigFormGroup.patchValue({ + clientCertificate: false + }, {emitEvent: false}); + break; + case SECURITY_CONFIG_MODE.X509: + this.jsonAllConfig.client = getDefaultClientSecurityConfigType(mode) as ClientSecurityConfigX509; + this.lwm2mConfigFormGroup.patchValue({ + clientCertificate: true + }, {emitEvent: false}); + break; + } + this.securityConfigClientUpdateValidators(mode); + } + + private securityConfigClientUpdateValidators = (mode: SECURITY_CONFIG_MODE): void => { + switch (mode) { + case SECURITY_CONFIG_MODE.NO_SEC: + this.setValidatorsNoSecX509(); + break; + case SECURITY_CONFIG_MODE.PSK: + this.lenMaxKeyClient = LEN_MAX_PSK; + this.setValidatorsPskRpk(mode); + break; + case SECURITY_CONFIG_MODE.RPK: + this.lenMaxKeyClient = LEN_MAX_PUBLIC_KEY_RPK; + this.setValidatorsPskRpk(mode); + break; + case SECURITY_CONFIG_MODE.X509: + this.lenMaxKeyClient = LEN_MAX_PUBLIC_KEY_RPK; + this.setValidatorsNoSecX509(); + break; + } + this.lwm2mConfigFormGroup.updateValueAndValidity(); + } + + private setValidatorsNoSecX509 = (): void => { + this.lwm2mConfigFormGroup.get('identityPSK').setValidators([]); + this.lwm2mConfigFormGroup.get('clientKey').setValidators([]); + } + + private setValidatorsPskRpk = (mode: SECURITY_CONFIG_MODE): void => { + if (mode === SECURITY_CONFIG_MODE.PSK) { + this.lwm2mConfigFormGroup.get('identityPSK').setValidators([Validators.required]); + } else { + this.lwm2mConfigFormGroup.get('identityPSK').setValidators([]); + } + this.lwm2mConfigFormGroup.get('clientKey').setValidators([Validators.required, + Validators.pattern(KEY_REGEXP_HEX_DEC), + Validators.maxLength(this.lenMaxKeyClient), Validators.minLength(this.lenMaxKeyClient)]); + } + + tabChanged = (tabChangeEvent: MatTabChangeEvent): void => { + if (this.tabIndexPrevious !== tabChangeEvent.index) { this.upDateValueToJson(); } + this.tabIndexPrevious = tabChangeEvent.index; + } + + private upDateValueToJson(): void { + switch (this.tabIndexPrevious) { + case 0: + this.upDateValueToJsonTab0(); + break; + case 1: + this.upDateValueToJsonTab1(); + break; + case 2: + this.upDateValueToJsonTab2(); + break; + } + } + + private upDateValueToJsonTab0 = (): void => { + if (this.lwm2mConfigFormGroup !== null) { + if (!this.lwm2mConfigFormGroup.get('endPoint').pristine && this.lwm2mConfigFormGroup.get('endPoint').valid) { + this.data.endPoint = this.lwm2mConfigFormGroup.get('endPoint').value; + // Client mode == PSK + if (this.lwm2mConfigFormGroup.get('securityConfigClientMode').value === SECURITY_CONFIG_MODE.PSK) { + const endPoint = 'endpoint'; + this.jsonAllConfig.client[endPoint] = this.data.endPoint; + this.jsonAllConfig.client[endPoint].markAsPristine({ + onlySelf: true + }); + this.upDateJsonAllConfig(); + } + } + /** only Client mode == PSK */ + if (!this.lwm2mConfigFormGroup.get('identityPSK').pristine && this.lwm2mConfigFormGroup.get('identityPSK').valid) { + this.lwm2mConfigFormGroup.get('identityPSK').markAsPristine({ + onlySelf: true + }); + this.updateIdentityPSK(); + } + /** only Client mode == PSK (len = 64) || RPK (len = 182) */ + if (!this.lwm2mConfigFormGroup.get('clientKey').pristine && this.lwm2mConfigFormGroup.get('clientKey').valid) { + this.lwm2mConfigFormGroup.get('clientKey').markAsPristine({ + onlySelf: true + }); + this.updateClientKey(); + } + } + } + + private upDateValueToJsonTab1 = (): void => { + if (this.lwm2mConfigFormGroup !== null) { + if (this.bootstrapFormGroup !== null && !this.bootstrapFormGroup.pristine && this.bootstrapFormGroup.valid) { + this.jsonAllConfig.bootstrap.bootstrapServer = this.bootstrapFormGroup.value; + this.bootstrapFormGroup.markAsPristine({ + onlySelf: true + }); + this.upDateJsonAllConfig(); + } + + if (this.lwm2mServerFormGroup !== null && !this.lwm2mServerFormGroup.pristine && this.lwm2mServerFormGroup.valid) { + this.jsonAllConfig.bootstrap.lwm2mServer = this.lwm2mServerFormGroup.value; + this.lwm2mServerFormGroup.markAsPristine({ + onlySelf: true + }); + this.upDateJsonAllConfig(); + } + } + } + + private upDateValueToJsonTab2 = (): void => { + if (!this.lwm2mConfigFormGroup.get(this.formControlNameJsonAllConfig).pristine && + this.lwm2mConfigFormGroup.get(this.formControlNameJsonAllConfig).valid) { + this.jsonAllConfig = this.lwm2mConfigFormGroup.get(this.formControlNameJsonAllConfig).value; + this.lwm2mConfigFormGroup.get(this.formControlNameJsonAllConfig).markAsPristine({ + onlySelf: true + }); + } + } + + private updateIdentityPSK = (): void => { + const securityMode = 'securityMode'; + if (this.lwm2mConfigFormGroup.get('bootstrapServer').value[securityMode] === SECURITY_CONFIG_MODE.PSK) { + this.lwm2mConfigFormGroup.get('bootstrapFormGroup').patchValue({ + clientPublicKeyOrId: this.lwm2mConfigFormGroup.get('identityPSK').value + }); + const identity = 'identity'; + this.jsonAllConfig.client[identity] = this.lwm2mConfigFormGroup.get('identityPSK').value; + this.upDateJsonAllConfig(); + } + if (this.lwm2mConfigFormGroup.get('lwm2mServer').value[securityMode] === SECURITY_CONFIG_MODE.PSK) { + this.lwm2mConfigFormGroup.get('lwm2mServerFormGroup').patchValue({ + clientPublicKeyOrId: this.lwm2mConfigFormGroup.get('identityPSK').value + }); + this.jsonAllConfig.bootstrap.lwm2mServer.clientPublicKeyOrId = this.lwm2mConfigFormGroup.get('identityPSK').value; + this.upDateJsonAllConfig(); + } + } + + private updateClientKey = (): void => { + const key = 'key'; + const securityMode = 'securityMode'; + this.jsonAllConfig.client[key] = this.lwm2mConfigFormGroup.get('clientKey').value; + if (this.lwm2mConfigFormGroup.get('bootstrapServer').value[securityMode] === SECURITY_CONFIG_MODE.PSK) { + this.lwm2mConfigFormGroup.get('bootstrapServer').patchValue({ + clientSecretKey: this.jsonAllConfig.client[key] + }, {emitEvent: false}); + this.jsonAllConfig.bootstrap.bootstrapServer.clientSecretKey = this.jsonAllConfig.client[key]; + } + if (this.lwm2mConfigFormGroup.get('lwm2mServer').value[securityMode] === SECURITY_CONFIG_MODE.PSK) { + this.lwm2mConfigFormGroup.get('lwm2mServer').patchValue({ + clientSecretKey: this.jsonAllConfig.client[key] + }, {emitEvent: false}); + this.jsonAllConfig.bootstrap.lwm2mServer.clientSecretKey = this.jsonAllConfig.client[key]; + } + this.upDateJsonAllConfig(); + } + + private upDateJsonAllConfig = (): void => { + this.data.jsonAllConfig = JSON.parse(JSON.stringify(this.jsonAllConfig)); + this.lwm2mConfigFormGroup.patchValue({ + jsonAllConfig: JSON.parse(JSON.stringify(this.jsonAllConfig)) + }, {emitEvent: false}); + this.lwm2mConfigFormGroup.markAsDirty(); + } + + private initLwm2mConfigFormGroup = (): FormGroup => { + if (SECURITY_CONFIG_MODE[this.jsonAllConfig.client.securityConfigClientMode.toString()] === SECURITY_CONFIG_MODE.PSK) { + const endpoint = 'endpoint'; + this.data.endPoint = this.jsonAllConfig.client[endpoint]; + } + return this.fb.group({ + securityConfigClientMode: [SECURITY_CONFIG_MODE[this.jsonAllConfig.client.securityConfigClientMode.toString()], []], + identityPSK: ['', []], + clientKey: ['', []], + clientCertificate: [false, []], + bootstrapServer: [this.jsonAllConfig.bootstrap[this.bootstrapServer], []], + lwm2mServer: [this.jsonAllConfig.bootstrap[this.lwm2mServer], []], + bootstrapFormGroup: this.getServerGroup(), + lwm2mServerFormGroup: this.getServerGroup(), + endPoint: [this.data.endPoint, []], + jsonAllConfig: [this.jsonAllConfig, []] + }); + } + + private getServerGroup = (): FormGroup => { + return this.fb.group({ + securityMode: [this.fb.control(SECURITY_CONFIG_MODE.NO_SEC), []], + clientPublicKeyOrId: ['', []], + clientSecretKey: ['', []] + }); + } + + save(): void { + this.upDateValueToJson(); + this.data.endPoint = this.lwm2mConfigFormGroup.get('endPoint').value.split('\'').join(''); + this.data.jsonAllConfig = this.jsonAllConfig; + if (this.lwm2mConfigFormGroup.get('securityConfigClientMode').value === SECURITY_CONFIG_MODE.PSK) { + const identity = 'identity'; + this.data.endPoint = this.data.jsonAllConfig.client[identity]; + } + this.dialogRef.close(this.data); + } + + cancel(): void { + this.dialogRef.close(undefined); + } +} + + diff --git a/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.models.ts b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.models.ts new file mode 100644 index 0000000000..a42757d326 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/lwm2m/security-config.models.ts @@ -0,0 +1,187 @@ +/// +/// Copyright © 2016-2021 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. +/// + +export const JSON_ALL_CONFIG = 'jsonAllConfig'; +export const END_POINT = 'endPoint'; +export const DEFAULT_END_POINT = 'default_client_lwm2m_end_point_no_sec'; +export const BOOTSTRAP_SERVERS = 'servers'; +export const BOOTSTRAP_SERVER = 'bootstrapServer'; +export const LWM2M_SERVER = 'lwm2mServer'; +export const LEN_MAX_PSK = 64; +export const LEN_MAX_PRIVATE_KEY = 134; +export const LEN_MAX_PUBLIC_KEY_RPK = 182; +export const LEN_MAX_PUBLIC_KEY_X509 = 3000; +export const KEY_REGEXP_HEX_DEC = /^[-+]?[0-9A-Fa-f]+\.?[0-9A-Fa-f]*?$/; + + +export interface DeviceCredentialsDialogLwm2mData { + jsonAllConfig?: SecurityConfigModels; + endPoint?: string; +} + +export enum SECURITY_CONFIG_MODE { + PSK = 'PSK', + RPK = 'RPK', + X509 = 'X509', + NO_SEC = 'NO_SEC' +} + +export const SECURITY_CONFIG_MODE_NAMES = new Map( + [ + [SECURITY_CONFIG_MODE.PSK, 'Pre-Shared Key'], + [SECURITY_CONFIG_MODE.RPK, 'Raw Public Key'], + [SECURITY_CONFIG_MODE.X509, 'X.509 Certificate'], + [SECURITY_CONFIG_MODE.NO_SEC, 'No Security'], + ] +); + +export type ClientSecurityConfigType = + ClientSecurityConfigPSK + | ClientSecurityConfigRPK + | ClientSecurityConfigX509 + | ClientSecurityConfigNoSEC; + +export interface ClientSecurityConfigPSK { + securityConfigClientMode: string; + endpoint: string; + identity: string; + key: string; +} + +export interface ClientSecurityConfigRPK { + securityConfigClientMode: string; + key: string; +} + +export interface ClientSecurityConfigX509 { + securityConfigClientMode: string; + x509: boolean; +} + +export interface ClientSecurityConfigNoSEC { + securityConfigClientMode: string; +} + +export interface ServerSecurityConfig { + securityMode: string; + clientPublicKeyOrId?: string; + clientSecretKey?: string; +} + +interface BootstrapSecurityConfig { + bootstrapServer: ServerSecurityConfig; + lwm2mServer: ServerSecurityConfig; +} + +export interface SecurityConfigModels { + client: ClientSecurityConfigType; + bootstrap: BootstrapSecurityConfig; +} + +export function getDefaultClientSecurityConfigType(securityConfigMode: SECURITY_CONFIG_MODE, endPoint?: string): ClientSecurityConfigType { + let security: ClientSecurityConfigType; + switch (securityConfigMode) { + case SECURITY_CONFIG_MODE.PSK: + security = { + securityConfigClientMode: '', + endpoint: endPoint, + identity: endPoint, + key: '' + }; + break; + case SECURITY_CONFIG_MODE.RPK: + security = { + securityConfigClientMode: '', + key: '' + }; + break; + case SECURITY_CONFIG_MODE.X509: + security = { + securityConfigClientMode: '', + x509: true + }; + break; + case SECURITY_CONFIG_MODE.NO_SEC: + security = { + securityConfigClientMode: '' + }; + break; + } + security.securityConfigClientMode = securityConfigMode.toString(); + return security; +} + +export function getDefaultServerSecurityConfig(): ServerSecurityConfig { + return { + securityMode: SECURITY_CONFIG_MODE.NO_SEC.toString(), + clientPublicKeyOrId: '', + clientSecretKey: '' + }; +} + +function getDefaultBootstrapSecurityConfig(): BootstrapSecurityConfig { + return { + bootstrapServer: getDefaultServerSecurityConfig(), + lwm2mServer: getDefaultServerSecurityConfig() + }; +} + +export function getDefaultSecurityConfig(): SecurityConfigModels { + const securityConfigModels = { + client: getDefaultClientSecurityConfigType(SECURITY_CONFIG_MODE.NO_SEC), + bootstrap: getDefaultBootstrapSecurityConfig() + }; + return securityConfigModels; +} + +const isSecurityConfigModels = (p: any): p is SecurityConfigModels => + p.hasOwnProperty('client') && + isClientSecurityConfigType(p['client']) && + p.hasOwnProperty('bootstrap') && + isBootstrapSecurityConfig(p['bootstrap']); + +const isClientSecurityConfigType = (p: any): p is ClientSecurityConfigType => + p.hasOwnProperty('securityConfigClientMode') && + p.hasOwnProperty('endpoint') && + p.hasOwnProperty('identity') && + p.hasOwnProperty('key') && + p.hasOwnProperty('x509'); + +const isBootstrapSecurityConfig = (p: any): p is BootstrapSecurityConfig => + p.hasOwnProperty('bootstrapServer') && + isServerSecurityConfig(p['bootstrapServer']) && + p.hasOwnProperty('lwm2mServer') && + isServerSecurityConfig(p['lwm2mServer']); + +const isServerSecurityConfig = (p: any): p is ServerSecurityConfig => + p.hasOwnProperty('securityMode') && + p.hasOwnProperty('clientPublicKeyOrId') && + p.hasOwnProperty('clientSecretKey'); + +export function validateSecurityConfig(config: string): boolean { + try { + const securityConfig= JSON.parse(config); + if (isSecurityConfigModels(securityConfig)) { + return true; + } else { + return false; + } + } catch (e) { + return false; + } +} + + diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts index c1aa7764f0..003ba5d34a 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts @@ -14,11 +14,25 @@ /// limitations under the License. /// -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Injectable, NgModule } from '@angular/core'; +import { Resolve, RouterModule, Routes } from '@angular/router'; import { HomeLinksComponent } from './home-links.component'; import { Authority } from '@shared/models/authority.enum'; +import { Observable } from 'rxjs'; +import { HomeDashboard } from '@shared/models/dashboard.models'; +import { DashboardService } from '@core/http/dashboard.service'; + +@Injectable() +export class HomeDashboardResolver implements Resolve { + + constructor(private dashboardService: DashboardService) { + } + + resolve(): Observable { + return this.dashboardService.getHomeDashboard(); + } +} const routes: Routes = [ { @@ -31,12 +45,18 @@ const routes: Routes = [ label: 'home.home', icon: 'home' } + }, + resolve: { + homeDashboard: HomeDashboardResolver } } ]; @NgModule({ imports: [RouterModule.forChild(routes)], - exports: [RouterModule] + exports: [RouterModule], + providers: [ + HomeDashboardResolver + ] }) export class HomeLinksRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html index 85b5b388a7..f519d22637 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html @@ -15,23 +15,26 @@ limitations under the License. --> - - - - - {{section.name}} - - - - - - {{place.icon}} - - {{place.name}} - - - - - - - + + + + + + + {{section.name}} + + + + + + {{place.icon}} + + {{place.name}} + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss index a97e7083e0..20949642ab 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss @@ -15,6 +15,11 @@ */ @import '../../../../../scss/constants'; +:host { + width: 100%; + height: 100%; +} + :host ::ng-deep { .tb-home-links { .mat-headline { diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts index 7d5348c04f..7f485df45c 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts @@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { HomeSection } from '@core/services/menu.models'; +import { ActivatedRoute } from '@angular/router'; +import { HomeDashboard } from '@shared/models/dashboard.models'; @Component({ selector: 'tb-home-links', @@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit { cols = 2; + homeDashboard: HomeDashboard = this.route.snapshot.data.homeDashboard; + constructor(private menuService: MenuService, - public breakpointObserver: BreakpointObserver) { + public breakpointObserver: BreakpointObserver, + private route: ActivatedRoute) { } ngOnInit() { - this.updateColumnCount(); - this.breakpointObserver - .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']]) - .subscribe((state: BreakpointState) => this.updateColumnCount()); + if (!this.homeDashboard) { + this.updateColumnCount(); + this.breakpointObserver + .observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']]) + .subscribe((state: BreakpointState) => this.updateColumnCount()); + } } private updateColumnCount() { diff --git a/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts b/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts index e64b9b1768..644645c1a9 100644 --- a/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts @@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common'; import { HomeLinksRoutingModule } from './home-links-routing.module'; import { HomeLinksComponent } from './home-links.component'; import { SharedModule } from '@app/shared/shared.module'; +import { HomeComponentsModule } from '@home/components/home-components.module'; @NgModule({ declarations: @@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module'; imports: [ CommonModule, SharedModule, + HomeComponentsModule, HomeLinksRoutingModule ] }) diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html index 7ab52a373a..eaca7133a0 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.html +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.html @@ -63,6 +63,20 @@
+
+ + + {{ 'dashboard.home-dashboard-hide-toolbar' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss new file mode 100644 index 0000000000..3a4ec550cd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2021 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 "../../../../../scss/constants"; + +:host { + .tb-default-dashboard { + tb-dashboard-autocomplete { + @media #{$mat-gt-sm} { + padding-right: 12px; + } + + @media #{$mat-lt-md} { + padding-bottom: 12px; + } + } + mat-checkbox { + @media #{$mat-gt-sm} { + margin-top: 16px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts index a24a17e6bf..30c0dc36aa 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts @@ -23,11 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti import { TranslateService } from '@ngx-translate/core'; import { ContactBasedComponent } from '../../components/entity/contact-based.component'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-tenant', templateUrl: './tenant.component.html', - styleUrls: [] + styleUrls: ['./tenant.component.scss'] }) export class TenantComponent extends ContactBasedComponent { @@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent { tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]], additionalInfo: this.fb.group( { - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null], + homeDashboardHideToolbar: [entity && entity.additionalInfo && + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true] } ) } @@ -65,6 +69,11 @@ export class TenantComponent extends ContactBasedComponent { this.entityForm.patchValue({title: entity.title}); this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId}); this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); + this.entityForm.patchValue({additionalInfo: + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}}); + this.entityForm.patchValue({additionalInfo: + {homeDashboardHideToolbar: entity.additionalInfo && + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}}); } updateFormState() { diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.html b/ui-ngx/src/app/modules/home/pages/user/user.component.html index fbc5f3c1c7..63154139e3 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user.component.html +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.html @@ -19,13 +19,13 @@
diff --git a/ui-ngx/src/app/modules/home/pages/user/user.component.ts b/ui-ngx/src/app/modules/home/pages/user/user.component.ts index 585723b659..f6fc69b7d6 100644 --- a/ui-ngx/src/app/modules/home/pages/user/user.component.ts +++ b/ui-ngx/src/app/modules/home/pages/user/user.component.ts @@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model'; import { selectAuth } from '@core/auth/auth.selectors'; import { map } from 'rxjs/operators'; import { Authority } from '@shared/models/authority.enum'; -import { isUndefined } from '@core/utils'; +import { isDefinedAndNotNull, isUndefined } from '@core/utils'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @Component({ @@ -56,11 +56,11 @@ export class UserComponent extends EntityComponent { } isUserCredentialsEnabled(): boolean { - if (!this.entity || !this.entity.additionalInfo || isUndefined(this.entity.additionalInfo.userCredentialsEnabled)) { - return true; - } else { return this.entity.additionalInfo.userCredentialsEnabled === true; - } + } + + isUserCredentialPresent(): boolean { + return this.entity && this.entity.additionalInfo && isDefinedAndNotNull(this.entity.additionalInfo.userCredentialsEnabled); } buildForm(entity: User): FormGroup { @@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null], defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false], + homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null], + homeDashboardHideToolbar: [entity && entity.additionalInfo && + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true] } ) } @@ -89,6 +92,11 @@ export class UserComponent extends EntityComponent { {defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}}); this.entityForm.patchValue({additionalInfo: {defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}}); + this.entityForm.patchValue({additionalInfo: + {homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}}); + this.entityForm.patchValue({additionalInfo: + {homeDashboardHideToolbar: entity.additionalInfo && + isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}}); } } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html index 802bb8e65a..e48eba1ebf 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html @@ -35,6 +35,7 @@ [isEditActionEnabled]="true" [isExportActionEnabled]="true" [isRemoveActionEnabled]="!isReadOnly" + [disableWidgetInteraction]="true" [callbacks]="dashboardCallbacks"> diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts index 8e99a14016..0be34d5f39 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts @@ -84,6 +84,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { aliasController: IAliasController = new AliasController(this.utils, this.entityService, + this.translate, () => { return { getStateParams(): StateParams { return {}; diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html index 2d20022576..8e723d15a4 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.html @@ -28,7 +28,7 @@
- + login.email email diff --git a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts index 1515c4fc34..62b8798bf8 100644 --- a/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/reset-password-request.component.ts @@ -19,7 +19,7 @@ import { AuthService } from '@core/auth/auth.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { PageComponent } from '@shared/components/page.component'; -import { FormBuilder } from '@angular/forms'; +import { FormBuilder, Validators } from '@angular/forms'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TranslateService } from '@ngx-translate/core'; @@ -31,8 +31,8 @@ import { TranslateService } from '@ngx-translate/core'; export class ResetPasswordRequestComponent extends PageComponent implements OnInit { requestPasswordRequest = this.fb.group({ - email: [''] - }); + email: ['', [Validators.email, Validators.required]] + }, {updateOn: 'submit'}); constructor(protected store: Store, private authService: AuthService, @@ -45,12 +45,16 @@ export class ResetPasswordRequestComponent extends PageComponent implements OnIn } sendResetPasswordLink() { - this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe( - () => { - this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.password-link-sent-message'), - type: 'success' })); - } - ); + if (this.requestPasswordRequest.valid) { + this.authService.sendResetPasswordLink(this.requestPasswordRequest.get('email').value).subscribe( + () => { + this.store.dispatch(new ActionNotificationShow({ + message: this.translate.instant('login.password-link-sent-message'), + type: 'success' + })); + } + ); + } } } diff --git a/ui-ngx/src/app/shared/components/dashboard-select.component.ts b/ui-ngx/src/app/shared/components/dashboard-select.component.ts index 7357a1aa5a..64a8eb42c1 100644 --- a/ui-ngx/src/app/shared/components/dashboard-select.component.ts +++ b/ui-ngx/src/app/shared/components/dashboard-select.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { Component, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + Component, + forwardRef, + Inject, + Injector, + Input, + OnInit, + StaticProvider, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, of } from 'rxjs'; import { PageLink } from '@shared/models/page/page-link'; @@ -32,7 +42,7 @@ import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef import { BreakpointObserver } from '@angular/cdk/layout'; import { DOCUMENT } from '@angular/common'; import { WINDOW } from '@core/services/window.service'; -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ComponentPortal } from '@angular/cdk/portal'; import { DASHBOARD_SELECT_PANEL_DATA, DashboardSelectPanelComponent, @@ -186,12 +196,12 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit { overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector)); } - private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector { - const injectionTokens = new WeakMap([ - [DASHBOARD_SELECT_PANEL_DATA, data], - [OverlayRef, overlayRef] - ]); - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): Injector { + const providers: StaticProvider[] = [ + {provide: DASHBOARD_SELECT_PANEL_DATA, useValue: data}, + {provide: OverlayRef, useValue: overlayRef} + ]; + return Injector.create({parent: this.viewContainerRef.injector, providers}); } private updateView() { diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index 18162cbe8b..1dc64194d9 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -34,6 +34,7 @@ import { Subscription } from 'rxjs'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { FlowDirective } from '@flowjs/ngx-flow'; import { TranslateService } from '@ngx-translate/core'; +import { UtilsService } from '@core/services/utils.service'; @Component({ selector: 'tb-file-input', @@ -59,7 +60,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, noFileText = 'import.no-file'; @Input() - inputId = 'select'; + inputId = this.utils.guid(); @Input() allowedExtensions: string; @@ -114,6 +115,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, private propagateChange = null; constructor(protected store: Store, + private utils: UtilsService, public translate: TranslateService) { super(store); } diff --git a/ui-ngx/src/app/shared/components/js-func.component.html b/ui-ngx/src/app/shared/components/js-func.component.html index 6173cacb99..6721ea2279 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.html +++ b/ui-ngx/src/app/shared/components/js-func.component.html @@ -24,12 +24,16 @@ - +
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 596c2fcb36..b815ec6315 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -29,12 +29,16 @@ mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()"> {{'js-func.mini' | translate }} - +
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx index 876d93c6ff..c22779897e 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx @@ -28,12 +28,17 @@ class ThingsboardRadios extends React.Component {this.props.form.title} - { + { this.props.onChangeValidate(e); }}> {items} diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx index 571f36eab7..dcfe453b61 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx @@ -22,6 +22,7 @@ import { KeyLabelItem } from '@shared/components/json-form/react/json-form.models'; import { Mode } from 'rc-select/lib/interface'; +import { deepClone } from '@core/utils'; interface ThingsboardRcSelectState extends JsonFormFieldState { currentValue: KeyLabelItem | KeyLabelItem[]; @@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component {options} diff --git a/ui-ngx/src/app/shared/components/json-object-edit.component.ts b/ui-ngx/src/app/shared/components/json-object-edit.component.ts index 3573c69491..a5d45d16c7 100644 --- a/ui-ngx/src/app/shared/components/json-object-edit.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-edit.component.ts @@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; -import { guid } from '@core/utils'; +import { guid, isUndefined } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; import { getAce } from '@shared/models/ace/ace.models'; @@ -60,21 +60,27 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va @Input() fillHeight: boolean; - @Input() editorStyle: {[klass: string]: any}; + @Input() editorStyle: { [klass: string]: any }; + + @Input() sort: (key: string, value: any) => any; private requiredValue: boolean; + get required(): boolean { return this.requiredValue; } + @Input() set required(value: boolean) { this.requiredValue = coerceBooleanProperty(value); } private readonlyValue: boolean; + get readonly(): boolean { return this.readonlyValue; } + @Input() set readonly(value: boolean) { this.readonlyValue = coerceBooleanProperty(value); @@ -224,8 +230,12 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va this.contentValue = ''; this.objectValid = false; try { + if (this.modelValue) { - this.contentValue = JSON.stringify(this.modelValue, undefined, 2); + this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : + (key, objectValue) => { + return this.sort(key, objectValue); + }, 2); this.objectValid = true; } else { this.objectValid = !this.required; diff --git a/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts index 246f38b87a..f9ada15d64 100644 --- a/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts +++ b/ui-ngx/src/app/shared/components/mat-chip-draggable.directive.ts @@ -124,6 +124,7 @@ class DraggableChip { if (this.preventDrag) { event.preventDefault(); } else { + event.stopPropagation(); this.dragging = true; globalDraggingChipListId = this.chipListElement.id; this.chipListElement.classList.add(draggingClassName); @@ -159,6 +160,7 @@ class DraggableChip { } private onDragEnd(event: Event | any) { + event.stopPropagation(); this.dragging = false; globalDraggingChipListId = null; this.chipListElement.classList.remove(draggingClassName); diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html index 4261dec207..4782283d49 100644 --- a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html @@ -24,7 +24,7 @@ skip_previous
- +
{{ this.currentTime | date:'medium'}} @@ -47,8 +47,8 @@ pause_circle_outline - + {{speedValue}}
-
\ No newline at end of file +
diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts index 7cdcc42eec..c5bc1d61c3 100644 --- a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { interval } from 'rxjs'; import { filter } from 'rxjs/operators'; import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps/map-models'; @@ -24,9 +24,9 @@ import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/m templateUrl: './history-selector.component.html', styleUrls: ['./history-selector.component.scss'] }) -export class HistorySelectorComponent implements OnInit, OnChanges { +export class HistorySelectorComponent implements OnChanges { - @Input() settings: HistorySelectSettings + @Input() settings: HistorySelectSettings; @Input() minTime: number; @Input() maxTime: number; @Input() step = 1000; @@ -47,9 +47,6 @@ export class HistorySelectorComponent implements OnInit, OnChanges { constructor(private cd: ChangeDetectorRef) { } - ngOnInit(): void { - } - ngOnChanges() { this.maxTimeIndex = Math.ceil((this.maxTime - this.minTime) / this.step); this.currentTime = this.minTime === Infinity ? null : this.minTime; @@ -57,34 +54,34 @@ export class HistorySelectorComponent implements OnInit, OnChanges { play() { this.playing = true; - if (!this.interval) + if (!this.interval) { this.interval = interval(1000 / this.speed) .pipe( - filter(() => this.playing)).subscribe(() => { - this.index++; - this.currentTime = this.minTime + this.index * this.step; - if (this.index <= this.maxTimeIndex) { - this.cd.detectChanges(); - this.timeUpdated.emit(this.currentTime); - } - else { - this.interval.complete(); - } - }, err => { - console.error(err); - }, () => { - this.currentTime = this.index = this.minTimeIndex; + filter(() => this.playing) + ).subscribe(() => { + this.index++; + this.currentTime = this.minTime + this.index * this.step; + if (this.index <= this.maxTimeIndex) { + this.cd.detectChanges(); + this.timeUpdated.emit(this.currentTime); + } else { this.playing = false; - this.interval = null; + this.interval.complete(); this.cd.detectChanges(); - }); + } + }, err => { + console.error(err); + }, () => { + this.interval = null; + }); + } } - reeneble() { - if (this.playing) { - const position = this.index; + reInit() { + if (this.interval) { this.interval.complete(); - this.index = position; + } + if (this.playing) { this.play(); } } @@ -138,8 +135,9 @@ export class HistorySelectorComponent implements OnInit, OnChanges { this.pause(); } - changeIndex() { - this.currentTime = this.minTime + this.index * this.step; + changeIndex(index: number) { + this.index = index; + this.currentTime = this.minTime + index * this.step; this.timeUpdated.emit(this.currentTime); } } diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index 6e61680b09..e1be42eff3 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -14,7 +14,18 @@ /// limitations under the License. /// -import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + Component, + forwardRef, + Inject, + Injector, + Input, + OnDestroy, + OnInit, + StaticProvider, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; @@ -32,7 +43,7 @@ import { TimewindowPanelComponent, TimewindowPanelData } from '@shared/components/time/timewindow-panel.component'; -import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; +import { ComponentPortal } from '@angular/cdk/portal'; import { MediaBreakpoints } from '@shared/models/constants'; import { BreakpointObserver } from '@angular/cdk/layout'; import { WINDOW } from '@core/services/window.service'; @@ -229,12 +240,12 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces }); } - private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector { - const injectionTokens = new WeakMap([ - [TIMEWINDOW_PANEL_DATA, data], - [OverlayRef, overlayRef] - ]); - return new PortalInjector(this.viewContainerRef.injector, injectionTokens); + private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): Injector { + const providers: StaticProvider[] = [ + {provide: TIMEWINDOW_PANEL_DATA, useValue: data}, + {provide: OverlayRef, useValue: overlayRef} + ]; + return Injector.create({parent: this.viewContainerRef.injector, providers}); } registerOnChange(fn: any): void { diff --git a/ui-ngx/src/app/shared/components/toast.directive.ts b/ui-ngx/src/app/shared/components/toast.directive.ts index af4494338d..addecfb57a 100644 --- a/ui-ngx/src/app/shared/components/toast.directive.ts +++ b/ui-ngx/src/app/shared/components/toast.directive.ts @@ -20,9 +20,11 @@ import { Directive, ElementRef, HostBinding, Inject, + Injector, Input, NgZone, OnDestroy, Optional, + StaticProvider, ViewChild, ViewContainerRef } from '@angular/core'; @@ -34,7 +36,6 @@ import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { MatButton } from '@angular/material/button'; import Timeout = NodeJS.Timeout; -import { PortalInjector } from '@angular/cdk/portal'; @Directive({ selector: '[tb-toast]' @@ -138,10 +139,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy { this.toastComponentRef.destroy(); } }; - const injectionTokens = new WeakMap([ - [MAT_SNACK_BAR_DATA, data] - ]); - const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens); + const providers: StaticProvider[] = [ + {provide: MAT_SNACK_BAR_DATA, useValue: data} + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector); this.cd.detectChanges(); diff --git a/ui-ngx/src/app/shared/models/alias.models.ts b/ui-ngx/src/app/shared/models/alias.models.ts index df80125ae4..5c02089b04 100644 --- a/ui-ngx/src/app/shared/models/alias.models.ts +++ b/ui-ngx/src/app/shared/models/alias.models.ts @@ -16,14 +16,14 @@ import { EntityType } from '@shared/models/entity-type.models'; import { EntityId } from '@shared/models/id/entity-id'; -import { EntitySearchDirection, EntityTypeFilter } from '@shared/models/relation.models'; -import { EntityInfo } from './entity.models'; +import { EntitySearchDirection, RelationEntityTypeFilter } from '@shared/models/relation.models'; import { EntityFilter } from '@shared/models/query/query.models'; export enum AliasFilterType { singleEntity = 'singleEntity', entityList = 'entityList', entityName = 'entityName', + entityType = 'entityType', stateEntity = 'stateEntity', assetType = 'assetType', deviceType = 'deviceType', @@ -40,6 +40,7 @@ export const aliasFilterTypeTranslationMap = new Map( [ AliasFilterType.singleEntity, 'alias.filter-type-single-entity' ], [ AliasFilterType.entityList, 'alias.filter-type-entity-list' ], [ AliasFilterType.entityName, 'alias.filter-type-entity-name' ], + [ AliasFilterType.entityType, 'alias.filter-type-entity-type' ], [ AliasFilterType.stateEntity, 'alias.filter-type-state-entity' ], [ AliasFilterType.assetType, 'alias.filter-type-asset-type' ], [ AliasFilterType.deviceType, 'alias.filter-type-device-type' ], @@ -66,6 +67,10 @@ export interface EntityNameFilter { entityNameFilter?: string; } +export interface EntityTypeFilter { + entityType?: EntityType; +} + export interface StateEntityFilter { stateEntityParamName?: string; defaultStateEntity?: EntityId; @@ -92,7 +97,7 @@ export interface RelationsQueryFilter { defaultStateEntity?: EntityId; rootEntity?: EntityId; direction?: EntitySearchDirection; - filters?: Array; + filters?: Array; maxLevel?: number; fetchLastLevelOnly?: boolean; } @@ -129,6 +134,7 @@ export type EntityFilters = SingleEntityFilter & EntityListFilter & EntityNameFilter & + EntityTypeFilter & StateEntityFilter & AssetTypeFilter & DeviceTypeFilter & diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 1637890ce5..a92bc3d672 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo { configuration?: DashboardConfiguration; } +export interface HomeDashboard extends Dashboard { + hideDashboardToolbar: boolean; +} + +export interface HomeDashboardInfo { + dashboardId: DashboardId; + hideDashboardToolbar: boolean; +} + export function isPublicDashboard(dashboard: DashboardInfo): boolean { if (dashboard && dashboard.assignedCustomers) { return dashboard.assignedCustomers diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index fd293aae64..553d22f0ff 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -36,7 +36,7 @@ export enum DeviceProfileType { export enum DeviceTransportType { DEFAULT = 'DEFAULT', MQTT = 'MQTT', - // LWM2M = 'LWM2M' + LWM2M = 'LWM2M', SNMP = 'SNMP' } @@ -85,8 +85,8 @@ export const deviceTransportTypeTranslationMap = new Map( [ [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'], [DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'], - // [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint'] - [DeviceTransportType.SNMP, 'device-profile.transport-type-snmp-hint'], + [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint'], + [DeviceTransportType.SNMP, 'device-profile.transport-type-snmp-hint'] ] ); @@ -132,13 +132,13 @@ export const deviceTransportTypeConfigurationInfoMap = new Map( [ [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'], - [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'] + [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'], + [DeviceCredentialsType.LWM2M_CREDENTIALS, 'LwM2M Credentials'] ] ); diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index 96fb7d2925..92fe4634f5 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -108,7 +108,8 @@ export class PageLink { public toQuery(): string { let query = `?pageSize=${this.pageSize}&page=${this.page}`; if (this.textSearch && this.textSearch.length) { - query += `&textSearch=${this.textSearch}`; + const textSearch = encodeURIComponent(this.textSearch); + query += `&textSearch=${textSearch}`; } if (this.sortOrder) { query += `&sortProperty=${this.sortOrder.property}&sortOrder=${this.sortOrder.direction}`; diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 86d2ddd89b..6bc2d74487 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -35,14 +35,17 @@ export enum EntityKeyType { SERVER_ATTRIBUTE = 'SERVER_ATTRIBUTE', TIME_SERIES = 'TIME_SERIES', ENTITY_FIELD = 'ENTITY_FIELD', - ALARM_FIELD = 'ALARM_FIELD' + ALARM_FIELD = 'ALARM_FIELD', + CONSTANT = 'CONSTANT', + COUNT = 'COUNT' } export const entityKeyTypeTranslationMap = new Map( [ [EntityKeyType.ATTRIBUTE, 'filter.key-type.attribute'], [EntityKeyType.TIME_SERIES, 'filter.key-type.timeseries'], - [EntityKeyType.ENTITY_FIELD, 'filter.key-type.entity-field'] + [EntityKeyType.ENTITY_FIELD, 'filter.key-type.entity-field'], + [EntityKeyType.CONSTANT, 'filter.key-type.constant'] ] ); @@ -59,6 +62,8 @@ export function entityKeyTypeToDataKeyType(entityKeyType: EntityKeyType): DataKe return DataKeyType.entityField; case EntityKeyType.ALARM_FIELD: return DataKeyType.alarm; + case EntityKeyType.COUNT: + return DataKeyType.count; } } @@ -74,6 +79,8 @@ export function dataKeyTypeToEntityKeyType(dataKeyType: DataKeyType): EntityKeyT return EntityKeyType.ALARM_FIELD; case DataKeyType.entityField: return EntityKeyType.ENTITY_FIELD; + case DataKeyType.count: + return EntityKeyType.COUNT; } } @@ -285,6 +292,7 @@ export const dynamicValueSourceTypeTranslationMap = new Map { sourceType: DynamicValueSourceType; sourceAttribute: string; + inherit?: boolean; } export interface FilterPredicateValue { @@ -343,12 +351,14 @@ export interface KeyFilterPredicateInfo { export interface KeyFilter { key: EntityKey; valueType: EntityKeyValueType; + value?: string | number | boolean; predicate: KeyFilterPredicate; } export interface KeyFilterInfo { key: EntityKey; valueType: EntityKeyValueType; + value?: string | number | boolean; predicates: Array; } @@ -445,7 +455,9 @@ function simpleKeyFilterPredicateToText(translate: TranslateService, break; case FilterPredicateType.BOOLEAN: operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); - value = translate.instant(keyFilterPredicate.value.defaultValue ? 'value.true' : 'value.false'); + if (!dynamicValue) { + value = translate.instant(keyFilterPredicate.value.defaultValue ? 'value.true' : 'value.false'); + } break; } if (!dynamicValue) { @@ -465,6 +477,7 @@ export function keyFilterInfosToKeyFilters(keyFilterInfos: Array) const keyFilter: KeyFilter = { key, valueType: keyFilterInfo.valueType, + value: keyFilterInfo.value, predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) }; keyFilters.push(keyFilter); @@ -485,6 +498,7 @@ export function keyFiltersToKeyFilterInfos(keyFilters: Array): Array< keyFilterInfo = { key, valueType: keyFilter.valueType, + value: keyFilter.value, predicates: [] }; keyFilterInfoMap[infoKey] = keyFilterInfo; @@ -507,6 +521,7 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array { const keyFilter: KeyFilter = { key, valueType: keyFilterInfo.valueType, + value: keyFilterInfo.value, predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) }; keyFilters.push(keyFilter); @@ -708,13 +723,13 @@ export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntity export interface EntityCountQuery { entityFilter: EntityFilter; + keyFilters?: Array; } export interface AbstractDataQuery extends EntityCountQuery { pageLink: T; entityFields?: Array; latestValues?: Array; - keyFilters?: Array; } export interface EntityDataQuery extends AbstractDataQuery { diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index 2f0f2b456f..c9037a5dab 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -52,7 +52,7 @@ export const directionTypeTranslations = new Map( ] ); -export interface EntityTypeFilter { +export interface RelationEntityTypeFilter { relationType: string; entityTypes: Array; } @@ -68,7 +68,7 @@ export interface RelationsSearchParameters { export interface EntityRelationsQuery { parameters: RelationsSearchParameters; - filters: Array; + filters: Array; } export interface EntitySearchQuery { diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index b4a428daf8..3877488671 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -65,6 +65,7 @@ export interface UpdateMessage { } export const phoneNumberPattern = /^\+[1-9]\d{1,14}$/; +export const phoneNumberPatternTwilio = /^\+[1-9]\d{1,14}$|^(MG|PN).*$/; export enum SmsProviderType { AWS_SNS = 'AWS_SNS', diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 1e77285667..cfbde53233 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -23,7 +23,7 @@ import { map } from 'rxjs/operators'; import { NgZone } from '@angular/core'; import { AlarmData, - AlarmDataQuery, + AlarmDataQuery, EntityCountQuery, EntityData, EntityDataQuery, EntityKey, @@ -36,7 +36,8 @@ export enum DataKeyType { attribute = 'attribute', function = 'function', alarm = 'alarm', - entityField = 'entityField' + entityField = 'entityField', + count = 'count' } export enum LatestTelemetry { @@ -181,6 +182,11 @@ export class EntityDataCmd implements WebsocketCmd { } } +export class EntityCountCmd implements WebsocketCmd { + cmdId: number; + query?: EntityCountQuery; +} + export class AlarmDataCmd implements WebsocketCmd { cmdId: number; query?: AlarmDataQuery; @@ -194,6 +200,10 @@ export class EntityDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } +export class EntityCountUnsubscribeCmd implements WebsocketCmd { + cmdId: number; +} + export class AlarmDataUnsubscribeCmd implements WebsocketCmd { cmdId: number; } @@ -206,6 +216,8 @@ export class TelemetryPluginCmdsWrapper { entityDataUnsubscribeCmds: Array; alarmDataCmds: Array; alarmDataUnsubscribeCmds: Array; + entityCountCmds: Array; + entityCountUnsubscribeCmds: Array; constructor() { this.attrSubCmds = []; @@ -215,6 +227,8 @@ export class TelemetryPluginCmdsWrapper { this.entityDataUnsubscribeCmds = []; this.alarmDataCmds = []; this.alarmDataUnsubscribeCmds = []; + this.entityCountCmds = []; + this.entityCountUnsubscribeCmds = []; } public hasCommands(): boolean { @@ -224,7 +238,9 @@ export class TelemetryPluginCmdsWrapper { this.entityDataCmds.length > 0 || this.entityDataUnsubscribeCmds.length > 0 || this.alarmDataCmds.length > 0 || - this.alarmDataUnsubscribeCmds.length > 0; + this.alarmDataUnsubscribeCmds.length > 0 || + this.entityCountCmds.length > 0 || + this.entityCountUnsubscribeCmds.length > 0; } public clear() { @@ -235,6 +251,8 @@ export class TelemetryPluginCmdsWrapper { this.entityDataUnsubscribeCmds.length = 0; this.alarmDataCmds.length = 0; this.alarmDataUnsubscribeCmds.length = 0; + this.entityCountCmds.length = 0; + this.entityCountUnsubscribeCmds.length = 0; } public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { @@ -253,6 +271,10 @@ export class TelemetryPluginCmdsWrapper { preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); leftCount -= preparedWrapper.alarmDataCmds.length; preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount); + leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length; + preparedWrapper.entityCountCmds = this.popCmds(this.entityCountCmds, leftCount); + leftCount -= preparedWrapper.entityCountCmds.length; + preparedWrapper.entityCountUnsubscribeCmds = this.popCmds(this.entityCountUnsubscribeCmds, leftCount); return preparedWrapper; } @@ -280,40 +302,54 @@ export interface SubscriptionUpdateMsg extends SubscriptionDataHolder { errorMsg: string; } -export enum DataUpdateType { +export enum CmdUpdateType { ENTITY_DATA = 'ENTITY_DATA', - ALARM_DATA = 'ALARM_DATA' + ALARM_DATA = 'ALARM_DATA', + COUNT_DATA = 'COUNT_DATA' } -export interface DataUpdateMsg { +export interface CmdUpdateMsg { cmdId: number; - data?: PageData; - update?: Array; errorCode: number; errorMsg: string; - dataUpdateType: DataUpdateType; + cmdUpdateType: CmdUpdateType; +} + +export interface DataUpdateMsg extends CmdUpdateMsg { + data?: PageData; + update?: Array; } export interface EntityDataUpdateMsg extends DataUpdateMsg { - dataUpdateType: DataUpdateType.ENTITY_DATA; + cmdUpdateType: CmdUpdateType.ENTITY_DATA; } export interface AlarmDataUpdateMsg extends DataUpdateMsg { - dataUpdateType: DataUpdateType.ALARM_DATA; + cmdUpdateType: CmdUpdateType.ALARM_DATA; allowedEntities: number; totalEntities: number; } -export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | SubscriptionUpdateMsg; +export interface EntityCountUpdateMsg extends CmdUpdateMsg { + cmdUpdateType: CmdUpdateType.COUNT_DATA; + count: number; +} + +export type WebsocketDataMsg = AlarmDataUpdateMsg | EntityDataUpdateMsg | EntityCountUpdateMsg | SubscriptionUpdateMsg; export function isEntityDataUpdateMsg(message: WebsocketDataMsg): message is EntityDataUpdateMsg { - const updateMsg = (message as DataUpdateMsg); - return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ENTITY_DATA; + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ENTITY_DATA; } export function isAlarmDataUpdateMsg(message: WebsocketDataMsg): message is AlarmDataUpdateMsg { - const updateMsg = (message as DataUpdateMsg); - return updateMsg.cmdId !== undefined && updateMsg.dataUpdateType === DataUpdateType.ALARM_DATA; + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.ALARM_DATA; +} + +export function isEntityCountUpdateMsg(message: WebsocketDataMsg): message is EntityCountUpdateMsg { + const updateMsg = (message as CmdUpdateMsg); + return updateMsg.cmdId !== undefined && updateMsg.cmdUpdateType === CmdUpdateType.COUNT_DATA; } export class SubscriptionUpdate implements SubscriptionUpdateMsg { @@ -365,21 +401,28 @@ export class SubscriptionUpdate implements SubscriptionUpdateMsg { } } -export class DataUpdate implements DataUpdateMsg { +export class CmdUpdate implements CmdUpdateMsg { cmdId: number; errorCode: number; errorMsg: string; - data?: PageData; - update?: Array; - dataUpdateType: DataUpdateType; + cmdUpdateType: CmdUpdateType; - constructor(msg: DataUpdateMsg) { + constructor(msg: CmdUpdateMsg) { this.cmdId = msg.cmdId; this.errorCode = msg.errorCode; this.errorMsg = msg.errorMsg; + this.cmdUpdateType = msg.cmdUpdateType; + } +} + +export class DataUpdate extends CmdUpdate implements DataUpdateMsg { + data?: PageData; + update?: Array; + + constructor(msg: DataUpdateMsg) { + super(msg); this.data = msg.data; this.update = msg.update; - this.dataUpdateType = msg.dataUpdateType; } } @@ -400,6 +443,15 @@ export class AlarmDataUpdate extends DataUpdate { } } +export class EntityCountUpdate extends CmdUpdate { + count: number; + + constructor(msg: EntityCountUpdateMsg) { + super(msg); + this.count = msg.count; + } +} + export interface TelemetryService { subscribe(subscriber: TelemetrySubscriber); update(subscriber: TelemetrySubscriber); @@ -411,6 +463,7 @@ export class TelemetrySubscriber { private dataSubject = new ReplaySubject(1); private entityDataSubject = new ReplaySubject(1); private alarmDataSubject = new ReplaySubject(1); + private entityCountSubject = new ReplaySubject(1); private reconnectSubject = new Subject(); private zone: NgZone; @@ -420,6 +473,7 @@ export class TelemetrySubscriber { public data$ = this.dataSubject.asObservable(); public entityData$ = this.entityDataSubject.asObservable(); public alarmData$ = this.alarmDataSubject.asObservable(); + public entityCount$ = this.entityCountSubject.asObservable(); public reconnect$ = this.reconnectSubject.asObservable(); public static createEntityAttributesSubscription(telemetryService: TelemetryService, @@ -464,6 +518,7 @@ export class TelemetrySubscriber { this.dataSubject.complete(); this.entityDataSubject.complete(); this.alarmDataSubject.complete(); + this.entityCountSubject.complete(); this.reconnectSubject.complete(); } @@ -513,6 +568,18 @@ export class TelemetrySubscriber { } } + public onEntityCount(message: EntityCountUpdate) { + if (this.zone) { + this.zone.run( + () => { + this.entityCountSubject.next(message); + } + ); + } else { + this.entityCountSubject.next(message); + } + } + public onReconnected() { this.reconnectSubject.next(); } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 24ea528b46..33b12045c6 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -154,6 +154,7 @@ export interface WidgetTypeParameters { hasDataPageLink?: boolean; singleEntity?: boolean; warnOnPageDataOverflow?: boolean; + ignoreDataUpdateOnIntervalTick?: boolean; } export interface WidgetControllerDescriptor { @@ -246,13 +247,15 @@ export interface DataKey extends KeyInfo { export enum DatasourceType { function = 'function', - entity = 'entity' + entity = 'entity', + entityCount = 'entityCount' } export const datasourceTypeTranslationMap = new Map( [ [ DatasourceType.function, 'function.function' ], - [ DatasourceType.entity, 'entity.entity' ] + [ DatasourceType.entity, 'entity.entity' ], + [ DatasourceType.entityCount, 'entity.entities-count' ] ] ); @@ -345,6 +348,11 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { targetDashboardStateId?: string; openRightLayout?: boolean; openNewBrowserTab?: boolean; + openInSeparateDialog?: boolean; + dialogTitle?: string; + dialogHideDashboardToolbar?: boolean; + dialogWidth?: number; + dialogHeight?: number; setEntityId?: boolean; stateEntityParamName?: string; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index ba56ad72fc..a142a48e90 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -52,6 +52,7 @@ import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatListModule } from '@angular/material/list'; import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; import { GridsterModule } from 'angular-gridster2'; @@ -132,6 +133,7 @@ import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-t import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component'; import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component'; import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; +import { DndModule } from 'ngx-drag-drop'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { ContactComponent } from '@shared/components/contact.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; @@ -259,6 +261,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select MatStepperModule, MatAutocompleteModule, MatChipsModule, + MatListModule, GridsterModule, ClipboardModule, FlexLayoutModule.withConfig({addFlexToParent: false}), @@ -269,6 +272,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowModule, NgxFlowchartModule ], @@ -348,6 +352,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select MatStepperModule, MatAutocompleteModule, MatChipsModule, + MatListModule, GridsterModule, ClipboardModule, FlexLayoutModule, @@ -358,6 +363,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select HotkeyModule, ColorPickerModule, NgxHmCarouselModule, + DndModule, NgxFlowchartModule, ConfirmDialogComponent, AlertDialogComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json index 8483fb1113..28fb3a4b48 100644 --- a/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json +++ b/ui-ngx/src/assets/locale/locale.constant-cs_CZ.json @@ -54,7 +54,10 @@ "share-via": "Sdílet přes {{provider}}", "continue": "Pokračovat", "discard-changes": "Zahodit změny", - "download": "Stáhnout" + "download": "Stáhnout", + "next-with-label": "Další: {{label}}", + "read-more": "Zobrazit více", + "hide": "Skrýt" }, "aggregation": { "aggregation": "Agregace", @@ -77,6 +80,8 @@ "test-mail-sent": "Testovací zpráva byla úspěšně odeslána!", "base-url": "Základní URL", "base-url-required": "Hodnota Základní URL je povinná.", + "prohibit-different-url": "Zakázat použití názvu hosta z hlaviček požadavku klienta", + "prohibit-different-url-hint": "Toto nastavení by mělo být povoleno v produkčních prostředích. Pokud je zakázáno, může způsobit bezpečnostní problémy", "mail-from": "Email od", "mail-from-required": "Hodnota Email od je povinná.", "smtp-protocol": "SMTP protokol", @@ -99,6 +104,33 @@ "proxy-user": "Uživatel proxy", "proxy-password": "Heslo proxy", "send-test-mail": "Odeslat testovací zprávu", + "sms-provider": "Poskytovatel SMS", + "sms-provider-settings": "Nastavení poskytovatele SMS", + "sms-provider-type": "Typ poskytovatele SMS", + "sms-provider-type-required": "Typ poskytovatele SMS je povinný.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "aws-access-key-id": "AWS Access Key ID", + "aws-access-key-id-required": "AWS Access Key ID je povinný", + "aws-secret-access-key": "AWS Secret Access Key", + "aws-secret-access-key-required": "AWS Secret Access Key je povinný", + "aws-region": "AWS Region", + "aws-region-required": "AWS Region je povinný", + "number-from": "Telefonní číslo odesílatele", + "number-from-required": "Telefonní číslo Odesílatele je povinné.", + "number-to": "Telefonní číslo příjemce", + "number-to-required": "Telefonní číslo příjemce je povinné.", + "phone-number-hint": "Telefonní číslo ve formátu E.164, např. +19995550123", + "phone-number-pattern": "Neplatné telefonní číslo. Mělo by odpovídat formátu E.164, např. +19995550123.", + "sms-message": "SMS zpráva", + "sms-message-required": "SMS zpráva je povinná.", + "sms-message-max-length": "SMS zpráva nemůže být delší než 1600 znaků", + "twilio-account-sid": "Twilio Account SID", + "twilio-account-sid-required": "Twilio Account SID je povinné", + "twilio-account-token": "Twilio Account Token", + "twilio-account-token-required": "Twilio Account Token je povinný", + "send-test-sms": "Odeslat testovací SMS", + "test-sms-sent": "Testovací SMS úspěšně odeslána!", "security-settings": "Bezpečnostní nastavení", "password-policy": "Politika hesel", "minimum-password-length": "Minimální délka hesla", @@ -119,8 +151,74 @@ "general-policy": "Obecná politika", "max-failed-login-attempts": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu", "minimum-max-failed-login-attempts-range": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu nemůže být záporný", - "user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email" - }, + "user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email", + "domain-name": "Doménové jméno", + "domain-name-unique": "Doménové jméno a protokol musí být unikátní.", + "error-verification-url": "Doménové jméno by nemělo obsahovat symbol '/' ani ':'. Příklad: thingsboard.io", + "oauth2": { + "access-token-uri": "URI přístupového tokenu", + "access-token-uri-required": "URI přístupového tokenu je povinné.", + "activate-user": "Aktivovat uživatele", + "add-domain": "Přidat doménu", + "delete-domain": "Smazat doménu", + "add-provider": "Přidat poskytovatele", + "delete-provider": "Smazat poskytovatele", + "allow-user-creation": "Povolit vytvoření uživatele", + "always-fullscreen": "Vždy v režimu celé obrazovky", + "authorization-uri": "Autorizační URI", + "authorization-uri-required": "Autorizační URI je povinné.", + "client-authentication-method": "Metoda autentizace klienta", + "client-id": "ID klienta", + "client-id-required": "ID klienta je povinné.", + "client-secret": "Heslo klienta", + "client-secret-required": "Heslo klienta je povinné.", + "custom-setting": "Vlastní nastavení", + "customer-name-pattern": "Vzor názvu zákazníka", + "default-dashboard-name": "Název defaultního dashboardu", + "delete-domain-text": "Budťe opatrní, protože po potvrzení nebudou doména ani žádná data poskytovatele dostupné.", + "delete-domain-title": "Jste si jisti, že chcete smazat nastavení domény '{{domainName}}'?", + "delete-registration-text": "Buďte opatrní, protože po potvrzení nebudou data poskytovatele dostupná.", + "delete-registration-title": "Jste si jisti, že chcete smazat poskytovatele '{{name}}'?", + "email-attribute-key": "Atribut klíče email", + "email-attribute-key-required": "Atribut klíče email je povinný.", + "first-name-attribute-key": "Atribut klíče jméno", + "general": "Obecné", + "jwk-set-uri": "JSON Web Key URI", + "last-name-attribute-key": "Atribut klíče příjmení", + "login-button-icon": "Ikona tlačítka přihlášení", + "login-button-label": "Označení poskytovatele", + "login-button-label-placeholder": "Přihlásit se přes $(Provider label)", + "login-button-label-required": "Označení je povinné.", + "login-provider": "Poskytovatel přihlášení", + "mapper": "Mapper", + "new-domain": "Nová doména", + "oauth2": "OAuth2", + "redirect-uri-template": "Šablona URI přesměrování", + "copy-redirect-uri": "Zkopírovat URI přesměrování", + "registration-id": "ID registrace", + "registration-id-required": "ID registrace je povinné.", + "registration-id-unique": "Id registrace musí být v systému unikátní.", + "scope": "Rozsah", + "scope-required": "Rozsah je povinný.", + "tenant-name-pattern": "Vzor názvu tenanta", + "tenant-name-pattern-required": "Vzor názvu tenanta je povinný.", + "tenant-name-strategy": "Strategie názvu tenanta", + "type": "Typ mapperu", + "uri-pattern-error": "Neplatný formát URI.", + "url": "URL", + "url-pattern": "Neplatný formát URL.", + "url-required": "URL je povinná.", + "user-info-uri": "User info URI", + "user-info-uri-required": "User info URI je povinné.", + "user-name-attribute-name": "Atribut klíče název uživatele", + "user-name-attribute-name-required": "Atribut klíče název uživatele je povinný", + "protocol": "Protokol", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Povolit nastavení OAuth2" + } + }, "alarm": { "alarm": "Alarm", "alarms": "Alarmy", @@ -128,6 +226,8 @@ "no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.", "alarm-required": "Alarm je povinný", "alarm-status": "Stav alarmu", + "alarm-status-list": "Seznam stavů alarmu", + "any-status": "Všechny stavy", "search-status": { "ANY": "Všechny", "ACTIVE": "Aktivní", @@ -154,6 +254,8 @@ "end-time": "Datum ukončení", "ack-time": "Datum přijetí", "clear-time": "Datum vyřešení", + "alarm-severity-list": "Seznam závažností alarmu", + "any-severity": "Všechny závažnosti", "severity-critical": "Kritická", "severity-major": "Vysoká", "severity-minor": "Nízká", @@ -176,12 +278,16 @@ "clear-alarm-title": "Odstranit alarm", "clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?", "alarm-status-filter": "Filtr stavu alarmu", + "alarm-filter": "Filtr alarmu", "max-count-load": "Maximální počet nahraných alarmů (0 - neomezeně)", "max-count-load-required": "Maximální počet nahraných alarmů je povinný.", "max-count-load-error-min": "Minimální hodnota je 0.", "fetch-size": "Velikost dávky", "fetch-size-required": "Velikost dávky je povinná.", - "fetch-size-error-min": "Minimální hodnota je 10." + "fetch-size-error-min": "Minimální hodnota je 10.", + "alarm-type-list": "Seznam typů alarmu", + "any-type": "Všechny typy", + "search-propagated-alarms": "Vyhledat zpropagované alarmy" }, "alias": { "add": "Přidat alias", @@ -211,6 +317,7 @@ "filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", "filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity", "filter-type-entity-view-search-query-description": "Entitní pohledy typů {{entityViewTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "Stav využití Api", "entity-filter": "Filtr entity", "resolve-multiple": "Použít jako více entit", "filter-type": "Typ filtru", @@ -325,6 +432,59 @@ "no-attributes-text": "Žádné atributy nebyly nalezeny", "no-telemetry-text": "Žádná telemetrie nebyla nalezena" }, + "api-usage": { + "api-usage": "Využití Api", + "data-points": "Datové body", + "data-points-storage-days": "Dny uložení datových bodů", + "email": "Email", + "email-messages": "Emailové zprávy", + "email-messages-daily-activity": "Denní aktivita emailových zpráv", + "email-messages-hourly-activity": "Hodinová aktivita emailových zpráv", + "email-messages-monthly-activity": "Měsíční aktivita emailových zpráv", + "exceptions": "Výjimky", + "executions": "Zpracování", + "javascript": "JavaScript", + "javascript-executions": "JavaScript výjimky", + "javascript-functions": "JavaScript funkce", + "javascript-functions-daily-activity": "Denní aktivita JavaScript funkcí", + "javascript-functions-hourly-activity": "Hodinová aktivita JavaScript funkcí", + "javascript-functions-monthly-activity": "Měsíční aktivita JavaScript funkcí", + "latest-error": "Poslední chyba", + "messages": "Zprávy", + "permanent-failures": "${entityName} permanentní chyby", + "permanent-timeouts": "${entityName} permanentní timeouty", + "processing-failures": "${entityName} chyby zpracování", + "processing-failures-and-timeouts": "Chyby a timeouty zpracování", + "processing-timeouts": "${entityName} timeouty zpracování", + "queue-stats": "Statistiky fronty", + "rule-chain": "Řetěz pravidel", + "rule-engine": "Engine pro zpracování pravidel", + "rule-engine-daily-activity": "Denní aktivita enginu pro zpracování pravidel", + "rule-engine-executions": "Zpracování Enginu pro zpracování pravidel", + "rule-engine-hourly-activity": "Hodinová aktivita enginu pro zpracování pravidel", + "rule-engine-monthly-activity": "Měsíční aktivita enginu pro zpracování pravidel", + "rule-engine-statistics": "Statistiky enginu pro zpracování pravidel", + "rule-node": "Uzel pravidla", + "sms": "SMS", + "sms-messages": "SMS zprávy", + "sms-messages-daily-activity": "Denní aktivita SMS zpráv", + "sms-messages-hourly-activity": "Hodinová aktivita SMS zpráv", + "sms-messages-monthly-activity": "Měsíční aktivita SMS zpráv", + "successful": "${entityName} úspěšnost", + "telemetry": "Telemetrie", + "telemetry-persistence": "Uložení telemetrie", + "telemetry-persistence-daily-activity": "Denní aktivita uložení telemetrie", + "telemetry-persistence-hourly-activity": "Hodinová aktivita uložení telemetrie", + "telemetry-persistence-monthly-activity": "Měsíční aktivita uložení telemetrie", + "transport": "Přenos", + "transport-daily-activity": "Denní aktivita přenosu", + "transport-data-points": "Datové body přenosu", + "transport-hourly-activity": "Hodinová aktivita přenosu", + "transport-messages": "Zprávy přenosu", + "transport-monthly-activity": "Měsíční aktivita přenosu", + "view-details": "Zobrazit detail", + "view-statistics": "Zobrazit statistiky" + }, "audit-log": { "audit": "Audit", "audit-logs": "Záznamy auditu", @@ -363,7 +523,13 @@ "action-data": "Data akce", "failure-details": "Detail chyby", "search": "Prohledat záznamy auditu", - "clear-search": "Vymazat vyhledávání" + "clear-search": "Vymazat vyhledávání", + "type-assigned-from-tenant": "Odebráno tenantovi", + "type-assigned-to-tenant": "Přiřazeno tenantovi", + "type-provision-success": "Zřízení zařízení", + "type-provision-failure": "Selhání zřízení zařízení", + "type-timeseries-updated": "Aktualizace telemetrie", + "type-timeseries-deleted": "Smazání telemetrie" }, "confirm-on-exit": { "message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?", @@ -549,6 +715,7 @@ "title-color": "Barva názvu", "display-dashboards-selection": "Zobrazit výběr dashboardů", "display-entities-selection": "Zobrazit výběr entit", + "display-filters": "Zobrazit filtry", "display-dashboard-timewindow": "Zobrazit časové okno", "display-dashboard-export": "Zobrazit export", "import": "Importovat dashboard", @@ -615,6 +782,7 @@ "alarm": "Pole alarmu", "timeseries-required": "Časové řady entity jsou povinné.", "timeseries-or-attributes-required": "Časové řady / atributy entity jsou povinné.", + "alarm-fields-timeseries-or-attributes-required": "Pole alarmu nebo časové řady / atributy jsou povinné.", "maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }", "alarm-fields-required": "Pole alarmu jsou povinná.", "function-types": "Typy funkcí", @@ -706,6 +874,12 @@ "access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.", "rsa-key": "RSA veřejný klíč", "rsa-key-required": "RSA veřejný klíč je povinný.", + "client-id": "ID klienta", + "client-id-pattern": "Obsahuje neplatné znaky.", + "user-name": "Název uživatele", + "user-name-required": "Název uživatele je povinný.", + "client-id-or-user-name-necessary": "ID klienta a/nebo název uživatele jsou povinné", + "password": "Heslo", "secret": "Heslo", "secret-required": "Heslo je povinné.", "device-type": "Typ zařízení", @@ -724,19 +898,183 @@ "details": "Detail", "copyId": "Kopírovat Id zařízení", "copyAccessToken": "Kopírovat přístupový token", + "copy-mqtt-authentication": "Kopírovat přístupové údaje MQTT", "idCopiedMessage": "Id zařízení bylo zkopírováno do schránky", "accessTokenCopiedMessage": "Přístupový token zařízení byl zkopírován do schránky", + "mqtt-authentication-copied-message": "MQTT autentizace zařízení byla zkopírována do schránky", "assignedToCustomer": "Přiřazeno zákazníkovi", "unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení", "unable-delete-device-alias-text": "Alias zařízení '{{deviceAlias}}' nelze smazat, protože je používán následujícími widgety:
{{widgetsList}}", "is-gateway": "Je bránou", + "overwrite-activity-time": "Přepsat čas aktivity připojeného zařízení", "public": "Veřejné", "device-public": "Zařízení je veřejné", "select-device": "Vybrat zařízení", "import": "Importovat zařízení", "device-file": "Soubor zařízení", "search": "Vyhledat zařízení", - "selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }" + "selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }", + "device-configuration": "Konfigurace zařízení", + "transport-configuration": "Konfigurace přenosu", + "wizard": { + "device-wizard": "Průvodce zařízením", + "device-details": "Detail zařízení", + "new-device-profile": "Vytvořit nový profil zařízení", + "existing-device-profile": "Vybrat existující profil zařízení", + "specific-configuration": "Specifická konfigurace", + "customer-to-assign-device": "Přiřadit zařízení zákazníkovi", + "add-credential": "Přidat přístupový údaj" + } + }, + "device-profile": { + "device-profile": "Profil zařízení", + "device-profiles": "Profily zařízení", + "all-device-profiles": "Všechny", + "add": "Přidat profil zařízení", + "edit": "Editovat profil zařízení", + "device-profile-details": "Detail profilu zařízení", + "no-device-profiles-text": "Žádné profily zařízení nebyly nalezeny", + "search": "Vyhledat profily zařízení", + "selected-device-profiles": "Vybráno { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }", + "no-device-profiles-matching": "Žádný profil zařízení odpovídající '{{entity}}' nebyl nalezen.", + "device-profile-required": "Profil zařízení je povinný", + "idCopiedMessage": "Id profilu zařízení bylo zkopírováno do schránky", + "set-default": "Učinit profil zařízení defaultním", + "delete": "Smazat profil zařízení", + "copyId": "Kopírovat Id profilu zařízení", + "new-device-profile-name": "Název profilu zařízení", + "new-device-profile-name-required": "Název profilu zařízení je povinný.", + "name": "Název", + "name-required": "Název je povinný.", + "type": "Typ profilu", + "type-required": "Typ profilu je povinný.", + "type-default": "Defaultní", + "transport-type": "Typ přenosu", + "transport-type-required": "Typ přenosu je povinný.", + "transport-type-default": "Defaultní", + "transport-type-default-hint": "Podporuje základní MQTT, HTTP and CoAP přenos", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Umožňuje pokročilé nastavení MQTT přenosu", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "Typ transportu LWM2M", + "description": "Popis", + "default": "Defaultní", + "profile-configuration": "Konfigurace profilu", + "transport-configuration": "Konfigurace přenosu", + "default-rule-chain": "Defaultní řetěz pravidel", + "select-queue-hint": "Vyberte z rozbalovacího seznamu nebo přidejte vlastní název.", + "delete-device-profile-title": "Jste si jisti, že chcete smazat profil zařízení '{{deviceProfileName}}'?", + "delete-device-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil zařízení ani žádná související data obnovit.", + "delete-device-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }?", + "delete-device-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily zařízení odstraněny a žádná související data nebude možné obnovit.", + "set-default-device-profile-title": "Jste si jisti, že chcete profil zařízení '{{deviceProfileName}}' učinit defaultním?", + "set-default-device-profile-text": "Po potvrzení bude profil zařízení označen jako defaultní a bude použit pro nová zařízení bez specifikovaného profilu.", + "no-device-profiles-found": "Žádné profily zařízení nebyly nalezeny.", + "create-new-device-profile": "Vytvořit nový!", + "mqtt-device-topic-filters": "Filtry MQTT fronty zařízení", + "mqtt-device-topic-filters-unique": "Filtry MQTT fronty zařízení musí být unikátní.", + "mqtt-device-payload-type": "MQTT zpráva zařízení", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Typ zprávy je povinný.", + "support-level-wildcards": "Jsou podporovány jednoúrovňové [+] a víceúrovňové [#] zástupné znaky.", + "telemetry-topic-filter": "Filtr fronty telemetrie", + "telemetry-topic-filter-required": "Filtr fronty telemetrie je povinný.", + "attributes-topic-filter": "Filtr atributů fronty", + "attributes-topic-filter-required": "Filtr atributů fronty je povinný.", + "telemetry-proto-schema": "Proto schéma telemetrie", + "telemetry-proto-schema-required": "Proto schéma telemetrie je povinné.", + "attributes-proto-schema": "Atributy proto schémata", + "attributes-proto-schema-required": "Atributy proto schémata jsou povinné.", + "rpc-response-topic-filter": "Filtr fronty RPC odpovědi", + "rpc-response-topic-filter-required": "Filtr fronty RPC odpovědi je povinný.", + "not-valid-pattern-topic-filter": "Neplatný vzor filtru fronty", + "not-valid-single-character": "Neplatné použití jednoúrovňového zástupného znaku", + "not-valid-multi-character": "Neplatné použití víceúrovňového zástupného znaku", + "single-level-wildcards-hint": "[+] je vhodný pro jakoukoli úroveň filtru fronty. Př.: v1/devices/+/telemetry or +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] může nahradit filtr fronty a může se jednat o poslední symbol fronty. Př.: # or v1/devices/me/#.", + "alarm-rules": "Pravidla alarmu", + "alarm-rules-with-count": "Pravidla alarmu ({{count}})", + "no-alarm-rules": "Žádná pravidla alarmu nejsou konfigurována", + "add-alarm-rule": "Přidat pravidlo alarmu", + "edit-alarm-rule": "Editovat pravidlo alarmu", + "alarm-type": "Typ alarmu", + "alarm-type-required": "Typ alarmu je povinný.", + "alarm-type-unique": "Typ alarmu musí být v rámci pravidel alarmu profilu zařízení unikátní.", + "create-alarm-pattern": "Vytvořit {{alarmType}} alarm", + "create-alarm-rules": "Vytvořit pravidla alarmu", + "no-create-alarm-rules": "Nejsou konfigurovány žádné podmínky vytvoření", + "add-create-alarm-rule-prompt": "Přidejte prosím pravidlo vytvoření alarmu", + "clear-alarm-rule": "Pravidlo zrušení alarmu", + "no-clear-alarm-rule": "Není konfigurována žádná podmínka zrušení", + "add-create-alarm-rule": "Přidat podmínku vytvoření", + "add-clear-alarm-rule": "Přidat podmínku zrušení", + "select-alarm-severity": "Vybrat závažnost alarmu", + "alarm-severity-required": "Závažnost alarmu je povinná.", + "condition-duration": "Doba trvání podmínky", + "condition-duration-value": "Hodnota doby trvání", + "condition-duration-time-unit": "Jednotka času", + "condition-duration-value-range": "Hodnota doby trvání musí být v rozsahu od 1 do 2147483647.", + "condition-duration-value-pattern": "Doba trvání musí být celé číslo.", + "condition-duration-value-required": "Doba trvání je povinná.", + "condition-duration-time-unit-required": "Jednotka času je povinná.", + "advanced-settings": "Pokročilá nastavení", + "alarm-rule-details": "Detail", + "add-alarm-rule-details": "Přidat detail", + "propagate-alarm": "Propagovat alarm", + "alarm-rule-relation-types-list": "Typy vztahů ke zpropagování", + "alarm-rule-relation-types-list-hint": "Pokud nejsou vybrány žádné typy vztahů, alarmy budou propagovány bez filtru typu vztahu.", + "alarm-details": "Detail alarmu", + "alarm-rule-condition": "Podmínka pravidla alarmu", + "enter-alarm-rule-condition-prompt": "Přidejte prosím podmínku pravidla alarmu", + "edit-alarm-rule-condition": "Editovat podmínku pravidla alarmu", + "device-provisioning": "Zřízení zařízení", + "provision-strategy": "Strategie zřízení", + "provision-strategy-required": "Strategie zřízení je povinná.", + "provision-strategy-disabled": "Zakázáno", + "provision-strategy-created-new": "Povolit vytváření nových zařízení", + "provision-strategy-check-pre-provisioned": "Zkontrolovat předvytvořená zařízení", + "provision-device-key": "Klíč pro zřízení zařízení", + "provision-device-key-required": "Klíč pro zřízení zařízení je povinný.", + "copy-provision-key": "Kopírovat klíč pro zřízení", + "provision-key-copied-message": "Klíč pro zřízení byl zkopírován do schránky", + "provision-device-secret": "Heslo pro zřízení zařízení", + "provision-device-secret-required": "Heslo pro zřízení zařízení je povinné.", + "copy-provision-secret": "Kopírovat heslo pro zřízení", + "provision-secret-copied-message": "Heslo pro zřízení zařízení bylo zkopírováno do schránky", + "condition": "Podmínka", + "condition-type": "Typ podmínky", + "condition-type-simple": "Jednoduchá", + "condition-type-duration": "Doba trvání", + "condition-during": "V průběhu {{during}}", + "condition-type-repeating": "Opakování", + "condition-type-required": "Typ podmínky je povinný.", + "condition-repeating-value": "Počet událostí", + "condition-repeating-value-range": "Počet událostí musí být v rozsahu od 1 do 2147483647.", + "condition-repeating-value-pattern": "Počet událostí musí být celé číslo.", + "condition-repeating-value-required": "Počet událostí je povinný.", + "condition-repeat-times": "Opakování { count, plural, 1 {1 krát} other {# krát} }", + "schedule-type": "Typ plánovače", + "schedule-type-required": "Typ plánovače je povinný.", + "schedule": "Časový plán", + "edit-schedule": "Editovat časový plán alarmu", + "schedule-any-time": "Aktivní neustále", + "schedule-specific-time": "Aktivní v konkrétním čase", + "schedule-custom": "Vlastní", + "schedule-day": { + "monday": "Pondělí", + "tuesday": "Úterý", + "wednesday": "Středa", + "thursday": "Čtvrtek", + "friday": "Pátek", + "saturday": "Sobota", + "sunday": "Neděle" + }, + "schedule-days": "Dny", + "schedule-time": "Čas", + "schedule-time-from": "Od", + "schedule-time-to": "Do", + "schedule-days-of-week-required": "Musí být vybrán minimálně jeden den v týdnu." }, "dialog": { "close": "Zavřít dialog" @@ -757,7 +1095,7 @@ "entity-alias": "Alias entity", "unable-delete-entity-alias-title": "Alias entity nebylo možné smazat", "unable-delete-entity-alias-text": "Alias entity '{{entityAlias}}' nelze smazat, protože je používán následujícími widgety:
{{widgetsList}}", - "duplicate-alias-error": "Nalezen dupliticní alias '{{alias}}'.
Aliasy entit musí být v rámci dashboardu unikátní.", + "duplicate-alias-error": "Nalezen duplicitní alias '{{alias}}'.
Aliasy entit musí být v rámci dashboardu unikátní.", "missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.", "configure-alias": "Konfigurovat '{{alias}}' alias", "alias": "Alias", @@ -794,6 +1132,10 @@ "type-devices": "Zařízení", "list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }", "device-name-starts-with": "Zařízení, jejichž název začíná '{{prefix}}'", + "type-device-profile": "Profil zařízení", + "type-device-profiles": "Profily zařízení", + "list-of-device-profiles": "{ count, plural, 1 {Jeden profil zařízení} other {Seznam # profilů zařízení} }", + "device-profile-name-starts-with": "Profily zařízení, jejichž název začíná '{{prefix}}'", "type-asset": "Aktivum", "type-assets": "Aktiva", "list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }", @@ -814,6 +1156,10 @@ "type-tenants": "Tenanti", "list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }", "tenant-name-starts-with": "Tenanti, jejichž název začíná '{{prefix}}'", + "type-tenant-profile": "Profil tenanta", + "type-tenant-profiles": "Profily tenantů", + "list-of-tenant-profiles": "{ count, plural, 1 {Jeden profil tenanta} other {Seznam # profilů tenantů} }", + "tenant-profile-name-starts-with": "Profily tenantů, jejichž název začíná '{{prefix}}'", "type-customer": "Zákazník", "type-customers": "Zákazníci", "list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }", @@ -840,6 +1186,8 @@ "rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'", "type-current-customer": "Stávající zákazník", "type-current-tenant": "Stávající tenant", + "type-current-user": "Stávající uživatel", + "type-current-user-owner": "Vlastník stávajícího uživatele", "search": "Vyhledat entity", "selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno", "entity-name": "Název entity", @@ -847,7 +1195,8 @@ "details": "Detail entity", "no-entities-prompt": "Žádné entity nebyly nalezeny", "no-data": "Nelze zobrazit žádná data", - "columns-to-display": "Zobrazit sloupce" + "columns-to-display": "Zobrazit sloupce", + "type-api-usage-state": "Stav využití API" }, "entity-field": { "created-time": "Datum vytvoření", @@ -1048,7 +1397,7 @@ "anonymous": "Anonymní", "basic": "Základní", "pem": "PEM", - "ca-cert": "soubor CA certifikátu *", + "ca-cert": "Soubor CA certifikátu *", "private-key": "Soubor privátního klíče *", "cert": "Soubor certifikátu *", "no-file": "Žádný soubor nebyl vybrán.", @@ -1154,6 +1503,93 @@ "file": "Soubor rozšíření", "invalid-file-error": "Neplatný soubor rozšíření" }, + "filter": { + "add": "Přidat filtr", + "edit": "Editovat filtr", + "name": "Název filtru", + "name-required": "Název filtru je povinný.", + "duplicate-filter": "Filtr s identickým názvem již existuje.", + "filters": "Filtry", + "unable-delete-filter-title": "Smazat filtr není možné", + "unable-delete-filter-text": "Filtr '{{filter}}' není možné smazat, protože je používán následujícím widgetem(y):
{{widgetsList}}", + "duplicate-filter-error": "Nalezen duplicitní filtr '{{filter}}'.
Filtry musí být v rámci dashboardu unikátní.", + "missing-key-filters-error": "U filtru '{{filter}}' chybí klíčové filtry.", + "filter": "Filtr", + "editable": "Editovatelné", + "no-filters-found": "Žádné filtry nebyly nalezeny.", + "no-filter-text": "Není specifikován žádný filtr", + "add-filter-prompt": "Přidejte prosím filtr", + "no-filter-matching": "'{{filter}}' nebyl nalezen.", + "create-new-filter": "Vytvořit nový!", + "filter-required": "Filtr je povinný.", + "operation": { + "operation": "Operace", + "equal": "je rovno", + "not-equal": "není rovno", + "starts-with": "začíná na", + "ends-with": "končí na", + "contains": "obsahuje", + "not-contains": "neobsahuje", + "greater": "větší než", + "less": "menší než", + "greater-or-equal": "větší nebo rovno", + "less-or-equal": "menší nebo rovno", + "and": "a", + "or": "nebo" + }, + "ignore-case": "ignorovat velikost písmen", + "value": "Hodnota", + "remove-filter": "Odebrat filtr", + "preview": "Náhled filtru", + "no-filters": "Nejsou konfigurovány žádné filtry", + "add-filter": "Přidat filtr", + "add-complex-filter": "Přidat komplexní filtr", + "add-complex": "Přidat komplex", + "complex-filter": "Komplexní filtr", + "edit-complex-filter": "Editovat komplexní filtr", + "edit-filter-user-params": "Editovat filtr predikátu parametrů uživatele", + "filter-user-params": "Filtr predikátu parametrů uživatele", + "user-parameters": "Parametry uživatele", + "display-label": "Zobrazované označení", + "autogenerated-label": "Automaticky vygenerovat označení", + "order-priority": "Priority pořadí polí", + "key-filter": "Klíčový filtr", + "key-filters": "Klíčové filtry", + "key-name": "Název klíče", + "key-name-required": "Název klíče je povinný.", + "key-type": { + "key-type": "Typ klíče", + "attribute": "Atribut", + "timeseries": "Časové řady", + "entity-field": "Pole entity" + }, + "value-type": { + "value-type": "Typ hodnoty", + "string": "Řetězec", + "numeric": "Číslo", + "boolean": "Pravdivostní hodnota", + "date-time": "Datum a čas" + }, + "value-type-required": "Typ hodnoty klíče je povinný.", + "key-value-type-change-title": "Jste si jisti, že chcete změnit typ klíče hodnoty?", + "key-value-type-change-message": "Pokud potvrdíte nový typ hodnoty, všechny zadané klíčové filtry budou odstraněny.", + "no-key-filters": "Nejsou konfigurovány žádné klíčové filtry", + "add-key-filter": "Přidat klíčový filtr", + "remove-key-filter": "Odebrat klíčový filtr", + "edit-key-filter": "Editovat klíčový filtr", + "date": "Datum", + "time": "Čas", + "current-tenant": "Stávající tenant", + "current-customer": "Stávající zákazník", + "current-user": "Stávající uživatel", + "current-device": "Stávající zařízení", + "default-value": "Defaultní hodnota", + "dynamic-source-type": "Dynamický typ zdroje", + "no-dynamic-value": "Žádná dynamická hodnota", + "source-attribute": "Atribut zdroje", + "switch-to-dynamic-value": "Přepnout na dynamickou hodnotu", + "switch-to-default-value": "Přepnout na defaultní hodnotu" + }, "fullscreen": { "expand": "Rozšířit do režimu celé obrazovky", "exit": "Ukončit režim celé obrazovky", @@ -1286,6 +1722,7 @@ "entity-field": "Pole entity", "access-token": "Přístupový token", "isgateway": "Je bránou", + "activity-time-from-gateway-device": "Čas aktivity ze zařízení brány", "description": "Popis" }, "stepper-text":{ @@ -1329,6 +1766,7 @@ "legend": { "direction": "Směr legendy", "position": "Pozice legendy", + "sort-legend": "Setřídit datové klíče v legendě", "show-max": "Zobrazit max hodnotu", "show-min": "Zobrazit min hodnotu", "show-avg": "Zobrazit průměrnou hodnotu", @@ -1525,6 +1963,12 @@ "help": "Nápověda", "reset-debug-mode": "Resetovat režim ladění na všech uzlech" }, + "timezone": { + "timezone": "Časová zóna", + "select-timezone": "Vyberte časovou zónu", + "no-timezones-matching": "žádné časové zóny odpovídající '{{timezone}}' nebyly nalezeny.", + "timezone-required": "Časová zóna je povinná." + }, "queue": { "select_name": "Vybrat název fronty", "name": "Název fronty", @@ -1563,6 +2007,87 @@ "isolated-tb-core-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta", "isolated-tb-rule-engine-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta" }, + "tenant-profile": { + "tenant-profile": "Profil tenanta", + "tenant-profiles": "Profily tenantů", + "add": "Přidat profil tenanta", + "edit": "Editovat profil tenanta", + "tenant-profile-details": "Detail profilu tenanta", + "no-tenant-profiles-text": "Nebyly nalezeny žádné profily tenantů", + "search": "Vyhledat profily tenantů", + "selected-tenant-profiles": "Vybráno { count, plural, 1 {1 profilů tenantů} other {# profilů tenantů} }", + "no-tenant-profiles-matching": "Žádné profily tenantů odpovídající '{{entity}}' nebyly nalezeny.", + "tenant-profile-required": "Profil tenanta je povinný", + "idCopiedMessage": "Id profilu tenanta bylo zkopírováno do schránky", + "set-default": "Učinit profil tenanta defaultním", + "delete": "Smazat profil tenanta", + "copyId": "Kopírovat Id profilu tenanta", + "name": "Název", + "name-required": "Název je povinný.", + "data": "Data profilu", + "profile-configuration": "Konfigurace profilu", + "description": "Popis", + "default": "Defaultní", + "delete-tenant-profile-title": "Jste si jisti, že chcete smazat profil tenanta '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil tenanta ani žádná související data obnovit.", + "delete-tenant-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil tenanta} other {# profilů tenanta} }?", + "delete-tenant-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily tenantů odstraněny a žádná související data nebude možné obnovit.", + "set-default-tenant-profile-title": "Jste si jisti, že chcete učinit profil tenanta '{{tenantProfileName}}' defaultním?", + "set-default-tenant-profile-text": "Po potvrzení bude profil tenanta označen jako defaultní a bude použit pro nové tenanty bez specifikovaného profilu.", + "no-tenant-profiles-found": "Nebyly nalezeny žádné profily tenantů.", + "create-new-tenant-profile": "Vytvořit nový!", + "maximum-devices": "Maximální počet zařízení (0 - neomezeno)", + "maximum-devices-required": "Maximální počet zařízení je povinný.", + "maximum-devices-range": "Minimální počet zařízení nemůže být záporný", + "maximum-assets": "Maximální počet aktiv (0 - neomezeno)", + "maximum-assets-required": "Maximální počet aktiv je povinný.", + "maximum-assets-range": "Maximální počet aktiv nemůže být záporný", + "maximum-customers": "Maximální počet zákazníků (0 - neomezeno)", + "maximum-customers-required": "Maximální počet zákazníkůje povinný.", + "maximum-customers-range": "Maximální počet zákazníků nemůže být záporný", + "maximum-users": "Maximální počet uživatelů (0 - neomezeno)", + "maximum-users-required": "Maximální počet uživatelů je povinný.", + "maximum-users-range": "Maximální počet uživatelů nemůže být záporný", + "maximum-dashboards": "Maximální počet dashboardů (0 - neomezeno)", + "maximum-dashboards-required": "Maximální počet dashboardů je povinný.", + "maximum-dashboards-range": "Maximální počet dashboardů nemůže být záporný", + "maximum-rule-chains": "Maximální počet řetězů pravidel (0 - neomezeno)", + "maximum-rule-chains-required": "Maximální počet řetězů pravidel je povinný.", + "maximum-rule-chains-range": "Maximální počet řetězů pravidel nemůže být záporný", + "transport-tenant-msg-rate-limit": "Limit přenosu zpráv tenanta.", + "transport-tenant-telemetry-msg-rate-limit": "Limit přenosu zpráv telemetrie tenanta.", + "transport-tenant-telemetry-data-points-rate-limit": "Limit přenosu datových bodů telemetrie tenanta.", + "transport-device-msg-rate-limit": "Limit přenosu zpráv zařízení.", + "transport-device-telemetry-msg-rate-limit": "Limit přenosu zpráv zařízení telemetrie tenanta.", + "transport-device-telemetry-data-points-rate-limit": "Limit přenosu datových bodů zařízení telemetrie tenanta.", + "max-transport-messages": "Maximální počet zpráv přenosu (0 - neomezeno)", + "max-transport-messages-required": "Maximální počet zpráv přenosu je povinný.", + "max-transport-messages-range": "Maximální počet zpráv přenosu nemůže být záporný", + "max-transport-data-points": "Maximální počet datových bodů přenosu (0 - neomezeno)", + "max-transport-data-points-required": "Maximální počet datových bodů přenosu je povinný.", + "max-transport-data-points-range": "Maximální počet datových bodů přenosu nemůže být záporný", + "max-r-e-executions": "Maximální počet zpracování enginu pro zpracování pravidel (0 - neomezeno)", + "max-r-e-executions-required": "Maximální počet zpracování enginu pro zpracování pravidel je povinný.", + "max-r-e-executions-range": "Maximální počet zpracování enginu pro zpracování pravidel nemůže být záporný", + "max-j-s-executions": "Maximální počet JavaScript zpracování (0 - neomezeno)", + "max-j-s-executions-required": "Maximální počet JavaScript zpracování je povinný.", + "max-j-s-executions-range": "Maximální počet JavaScript zpracování nemůže být záporný", + "max-d-p-storage-days": "Maximální počet dnů uložení datových bodů (0 - neomezeno)", + "max-d-p-storage-days-required": "Maximální počet dnů uložení datových bodů je povinný.", + "max-d-p-storage-days-range": "Maximální počet dnů uložení datových bodů nemůže být záporný", + "default-storage-ttl-days": "Defaultní počet dnů TTL úložiště (0 - neomezeno)", + "default-storage-ttl-days-required": "Defaultní počet dnů TTL úložiště je povinný.", + "default-storage-ttl-days-range": "Defaultní počet dnů TTL úložiště nemůže být záporný", + "max-rule-node-executions-per-message": "Maximální počet zpracování uzlů pravidel na zprávu (0 - neomezeno)", + "max-rule-node-executions-per-message-required": "Maximální počet zpracování uzlů pravidel na zprávu je povinný.", + "max-rule-node-executions-per-message-range": "Maximální počet zpracování uzlů pravidel na zprávu nemůže být záporný", + "max-emails": "Maximální počet odeslaných emailů (0 - neomezeno)", + "max-emails-required": "Maximální počet odeslaných emailů je povinný.", + "max-emails-range": "Maximální počet odeslaných emailů nemůže být záporný", + "max-sms": "Maximální počet odeslaných SMS (0 - neomezeno)", + "max-sms-required": "Maximální počet odeslaných SMS je povinný.", + "max-sms-range": "Maximální počet odeslaných SMS nemůže být záporný" + }, "timeinterval": { "seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }", "minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }", @@ -1574,8 +2099,14 @@ "seconds": "Vteřiny", "advanced": "Rozšířené" }, + "timeunit": { + "seconds": "Vteřiny", + "minutes": "Minuty", + "hours": "Hodiny", + "days": "Dny" + }, "timewindow": { - "days": "{ days, plural, 1 { den } other {# days } }", + "days": "{ days, plural, 1 { den } other {# dnů } }", "hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }", "minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }", "seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }", @@ -1694,6 +2225,7 @@ "type": "Typ widgetu", "resources": "Zdroje", "resource-url": "JavaScript/CSS URL", + "resource-is-module": "Je modulem", "remove-resource": "Odebrat zdroj", "add-resource": "Přidat zdroj", "html": "HTML", @@ -1711,7 +2243,10 @@ "widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!", "add": "Přidat widget", "undo": "Vrátit změny widgetu", - "export": "Exportovat widget" + "export": "Exportovat widget", + "no-data": "Nejsou k dispozici žádná data pro zobrazení ve widgetu", + "data-overflow": "Widget zobrazuje {{count}} z {{total}} entit", + "alarm-data-overflow": "Widget zobrazuje alarmy {{allowedEntities}} (maxima možných) entit z {{totalEntities}} entit" }, "widget-action": { "header-button": "Tlačítko hlavičky widgetu", @@ -1724,7 +2259,14 @@ "target-dashboard-state-required": "Cílový stav dashboardu je povinný", "set-entity-from-widget": "Nastavit entitu z widgetu", "target-dashboard": "Cílový dashboard", - "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)" + "open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)", + "open-in-separate-dialog": "Otevřít v samostatném okně", + "dialog-title": "Název okna", + "dialog-hide-dashboard-toolbar": "Skrýt v okně nástrojovou lištu dashboardu", + "dialog-width": "Šířka okna v procentech vzhledem k šířce obrazovky", + "dialog-height": "Výška okna v procentech vzhledem k výšce obrazovky", + "dialog-size-range-error": "Hodnota procentuální velikosti musí být v rozsahu od 1 do 100.", + "open-new-browser-tab": "Otevřít na nové záložce prohlížeče" }, "widgets-bundle": { "current": "Vybraná kategorie", @@ -1891,8 +2433,11 @@ "entity-coordinate-required": "Obě pole, zeměpisná šířka i zeměpisná délka, jsou povinná", "entity-timeseries-required": "Časové řady entity jsou povinné", "get-location": "Získat aktuální polohu", + "invalid-date": "Neplatné datum", "latitude": "Zeměpisná šířka", "longitude": "Zeměpisná délka", + "min-value-error": "Minimální hodnota je {{value}}", + "max-value-error": "Maximální hodnota je {{value}}", "not-allowed-entity": "Vybraná entita nemůže mít sdílené atributy", "no-attribute-selected": "Není vybrán žádný atribut", "no-datakey-selected": "Není vybrán žádný datový klíč", @@ -1900,7 +2445,10 @@ "no-entity-selected": "Není vybrána žádná entita", "no-image": "Žádný obrázek", "no-support-geolocation": "Váš prohlížeč nepodporuje geolokaci", - "no-support-web-camera": "Žádná podporovaná webová kamera", + "no-support-web-camera": "Váš prohlížeč nepodporuje kamery", + "enable-https-use-widget": "Prosím povolte HTTPS abyste mohli používat tento widget", + "no-found-your-camera": "Nelze nalézt vyši kameru", + "no-permission-camera": "Přístup byl zakázán uživatelem / Tato stránka nemá oprávnění použít kameru", "no-timeseries-selected": "Nejsou vybrány žádné časové řady", "secret-key": "Tajný klíč", "secret-key-required": "Tajný klíč je povinný", diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index 2df1635fd2..b65a875aed 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -2056,9 +2056,7 @@ "name-required": "Απαιτείται Όνομα", "configuration": "Διαμόρφωση", "schedule": "Πρόγραμμα", - "start": "Έναρξη", - "date": "Ημερομηνία", - "time": "Ώρα", + "start-time": "Ώρα έναρξης", "repeat": "Επανάληψη", "repeats": "Επαναλήψεις", "daily": "Καθημερινά", 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 b4600cc5a7..0f7f94154b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -74,6 +74,7 @@ "admin": { "general": "General", "general-settings": "General Settings", + "home-settings": "Home Settings", "outgoing-mail": "Mail Server", "outgoing-mail-settings": "Outgoing Mail Server Settings", "system-settings": "System Settings", @@ -121,7 +122,9 @@ "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", @@ -298,6 +301,7 @@ "filter-type-single-entity": "Single entity", "filter-type-entity-list": "Entity list", "filter-type-entity-name": "Entity name", + "filter-type-entity-type": "Entity type", "filter-type-state-entity": "Entity from dashboard state", "filter-type-state-entity-description": "Entity taken from dashboard state parameters", "filter-type-asset-type": "Asset type", @@ -764,7 +768,9 @@ "select-state": "Select target state", "state-controller": "State controller", "search": "Search dashboards", - "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected" + "selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected", + "home-dashboard": "Home dashboard", + "home-dashboard-hide-toolbar": "Hide home dashboard toolbar" }, "datakey": { "settings": "Settings", @@ -797,6 +803,7 @@ "datasource": { "type": "Datasource type", "name": "Name", + "label": "Label", "add-datasource-prompt": "Please add datasource" }, "details": { @@ -874,6 +881,31 @@ "access-token-invalid": "Access token length must be from 1 to 20 characters.", "rsa-key": "RSA public key", "rsa-key-required": "RSA public key is required.", + "lwm2m-key": "LwM2M Security config key", + "lwm2m-key-required": "LwM2M Security config key is required.", + "lwm2m-value": "LwM2M Security config", + "lwm2m-value-required": "LwM2M Security config value is required.", + "lwm2m-value-json-error": "LwM2M Security config value is not json format.", + "lwm2m-endpoint": "Client endpoint/identity", + "lwm2m-security-info": "Security Config Info", + "lwm2m-value-edit": "Edit Security config", + "lwm2m-value-edit-tip": "Edit security config json editor", + "lwm2m-security-config": { + "identity": "Client Identity", + "client-key": "Client Key", + "required": " value is required.", + "endpoint": "Endpoint Client Name", + "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-secret-key": "Client Secret Key", + "config-json-tab": "Json Client Security Config", + "pattern_hex_dec": "{ count, plural, 0 {must be hex decimal format} other {must be # characters} }" + }, "client-id": "Client ID", "client-id-pattern": "Contains invalid character.", "user-name": "User Name", @@ -906,6 +938,7 @@ "unable-delete-device-alias-title": "Unable to delete device alias", "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):
{{widgetsList}}", "is-gateway": "Is gateway", + "overwrite-activity-time": "Overwrite activity time for connected device", "public": "Public", "device-public": "Device is public", "select-device": "Select device", @@ -1082,7 +1115,57 @@ "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." + "schedule-days-of-week-required": "At least one day of week should be selected.", + "lwm2m": { + "client-only-observe-after-connect": "{ count, plural, 1 {Strategy 1: Only Observe Request to the client after the initial connection} other {Strategy 2: Read All Resources & Observe Request to the client after registration} }", + "client-only-observe-after-connect-tip": "{ count, plural, 1 {Strategy 1: After the initial connection of the LWM2M Client, the server sends Observe resources Request to the client, those resources that are marked as observation in the Device profile and which exist on the LWM2M client.} other {Strategy 2: After the registration, request the client to read all the resource values for all objects that the LWM2M client has,\n then execute: the server sends Observe resources Request to the client, those resources that are marked as observation in the Device profile and which exist on the LWM2M client.} }", + "object-list": "Object list", + "object-list-empty": "No objects selected.", + "no-objects-matching": "No objects matching '{{object}}' were found.", + "valid-id-instance-no-min": "Instance number '{{instance}}' no validated. Min value='{{min}}'", + "valid-id-instance-no-max": "Instance number '{{instance}}' no validated. Max value='{{max}}'", + "valid-id-instance": "Instance number '{{instance}}' no validated. { count, plural, 1 {Max value='{{max}}'} 2 {Min value='{{min}}'} other {Must be only number} }", + "model-tab": "LWM2M Model", + "add-instances-tip": "Add new instances", + "instances-list": "Instances list", + "instances-input": "Input Instance Id value", + "instances-input-holder": "Input Instance number...", + "resource-label": "Resource", + "observe-label": "Observe", + "attribute-label": "Attribute", + "telemetry-label": "Telemetry", + "key-name-label": "Key Name", + "is-observe-tip": "Is Observe", + "is-attr-tip": "Is Attribute", + "is-telemetry-tip": "Is Telemetry", + "key-name-tip": "Key Name", + "key-name": "Key Name", + "key-name_label": "Key Name in Camel format", + "required": " value is required.", + "mode": "Security config mode", + "pattern_hex_dec": "{ count, plural, 0 {must be hex decimal format} other {must be # characters} }", + "bootstrap-tab": "Bootstrap", + "servers": "Servers", + "short-id": "Short ID", + "short-id-tip": "Short Server ID", + "lifetime": "Lifetime of the registration for this LwM2M client", + "default-min-period": "Minimum Period between two notifications (sec)", + "notif-if-disabled": "Notification Storing When Disabled or Offline", + "binding": "Binding", + "bootstrap-server": "Bootstrap Server", + "lwm2m-server": "LwM2M Server", + "server-host": "Host", + "server-host-tip": "Server Host", + "server-port": "Port", + "server-port-tip": "Server Port", + "server-public-key": "Server Public Key", + "server-public-key-tip": "Server Public Key only for X509, RPK", + "client-hold-off-time": "Hold Off Time", + "client-hold-off-time-tip": "Client Hold Off Time for use with a Bootstrap-Server only", + "bootstrap-server-account-timeout": "Account after the timeout", + "bootstrap-server-account-timeout-tip": "Bootstrap-Server Account after the timeout value given by this resource.", + "config-json-tab": "Json Config Profile Device" + } }, "dialog": { "close": "Close dialog" @@ -1099,6 +1182,7 @@ "entity": { "entity": "Entity", "entities": "Entities", + "entities-count": "Entities count", "aliases": "Entity aliases", "entity-alias": "Entity alias", "unable-delete-entity-alias-title": "Unable to delete entity alias", @@ -1569,7 +1653,8 @@ "key-type": "Key type", "attribute": "Attribute", "timeseries": "Timeseries", - "entity-field": "Entity field" + "entity-field": "Entity field", + "constant": "Constant" }, "value-type": { "value-type": "Value type", @@ -1596,7 +1681,9 @@ "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" + "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": "Expand to fullscreen", @@ -1730,6 +1817,7 @@ "entity-field": "Entity field", "access-token": "Access token", "isgateway": "Is Gateway", + "activity-time-from-gateway-device": "Activity time from gateway device", "description": "Description" }, "stepper-text":{ @@ -2267,6 +2355,12 @@ "set-entity-from-widget": "Set entity from widget", "target-dashboard": "Target dashboard", "open-right-layout": "Open right dashboard layout (mobile view)", + "open-in-separate-dialog": "Open in separate dialog", + "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.", "open-new-browser-tab": "Open in a new browser tab" }, "widgets-bundle": { diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 91286c0c3f..3162786211 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -4,9 +4,11 @@ "unauthorized-access": "Acceso no autorizado", "unauthorized-access-text": "Debes iniciar sesión para tener acceso a este recurso!", "access-forbidden": "Acceso Prohibido", - "access-forbidden-text": "No tienes derechos para acceder a esta ubicación!
Intenta iniciar sesión con otro usuario si todavía quieres acceder a esta ubicación.", + "access-forbidden-text": "No tienes suficientes derechos para acceder a esta ubicación!
Intenta iniciar sesión con otro usuario si todavía quieres acceder a esta ubicación.", "refresh-token-expired": "La sesión ha expirado", - "refresh-token-failed": "No se puede actualizar la sesión" + "refresh-token-failed": "No se puede actualizar la sesión", + "permission-denied": "Permiso Denegado", + "permission-denied-text": "No tienes suficientes derechos para realizar esta operación!" }, "action": { "activate": "Activar", @@ -21,6 +23,7 @@ "no": "No", "update": "Actualizar", "remove": "Eliminar", + "select": "Seleccionar", "search": "Buscar", "clear-search": "Borrar búsqueda", "assign": "Asignar", @@ -49,13 +52,16 @@ "import": "Importar", "export": "Exportar", "share-via": "Compartir vía {{provider}}", - "discard-changes": "Cancelar los cambios", "continue": "Continuar", - "download": "Descargar" + "discard-changes": "Cancelar cambios", + "download": "Descargar", + "next-with-label": "Siguiente: {{label}}", + "read-more": "Leer más", + "hide": "Ocultar" }, "aggregation": { - "aggregation": "Agregación", - "function": "Función de Agregación", + "aggregation": "Agrupación", + "function": "Función de Agrupación", "limit": "Valores Max", "group-interval": "Intervalo de agrupación", "min": "Min", @@ -74,6 +80,8 @@ "test-mail-sent": "Mail de prueba enviado correctamente!", "base-url": "URL Base", "base-url-required": "URL Base requerida.", + "prohibit-different-url": "Prohibir el uso de hostname en cabeceras de request del cliente", + "prohibit-different-url-hint": "Este ajuste debe ser activado en entornos de producción. Puede causar fallos de seguridad si está desactivado", "mail-from": "Mail Desde", "mail-from-required": "Mail Desde requerido.", "smtp-protocol": "Protocolo SMTP", @@ -87,9 +95,44 @@ "timeout-invalid": "No parece un Timeout valido.", "enable-tls": "Habilitar TLS", "tls-version": "Versión TLS", + "enable-proxy": "Habilitar proxy", + "proxy-host": "Host proxy", + "proxy-host-required": "Se requiere host Proxy.", + "proxy-port": "Puerto proxy", + "proxy-port-required": "Se requiere puerto proxy.", + "proxy-port-range": "El puerto proxy debe estar en un rango de 1 a 65535.", + "proxy-user": "Usuario proxy", + "proxy-password": "Contraseña proxy", "send-test-mail": "Enviar correo de prueba", - "password-policy": "Política de contraseñas", + "sms-provider": "Proveedor SMS", + "sms-provider-settings": "Ajustes proveedor SMS", + "sms-provider-type": "Tipo de proveedor SMS", + "sms-provider-type-required": "Se requiere proveedor SMS.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "aws-access-key-id": "AWS Access Key ID", + "aws-access-key-id-required": "Se requiere AWS Access Key ID", + "aws-secret-access-key": "AWS Secret Access Key", + "aws-secret-access-key-required": "Se requere AWS Secret Access Key", + "aws-region": "Región AWS", + "aws-region-required": "Se requere región AWS", + "number-from": "Nº de teléfono Origen", + "number-from-required": "Se requere Nº de teléfono origen.", + "number-to": "Nº de teléfono de destino", + "number-to-required": "Se requere Nº de teléfono de destino.", + "phone-number-hint": "Nº de teléfono en formato E.164, ex. +19995550123", + "phone-number-pattern": "Nº Inválido. Debe estar en formato E.164, ex. +19995550123.", + "sms-message": "Mensaje SMS", + "sms-message-required": "Se requeriere mensaje SMS.", + "sms-message-max-length": "Los SMS no pueden ser más largos de 1600 caracteres", + "twilio-account-sid": "SID de cuenta Twilio", + "twilio-account-sid-required": "Se requere SID de cuenta Twilio", + "twilio-account-token": "Token de cuenta Twilio", + "twilio-account-token-required": "Se requiere Token Twilio", + "send-test-sms": "Enviar SMS de prueba", + "test-sms-sent": "SMS enviado con éxito!", "security-settings": "Configuraciones de seguridad", + "password-policy": "Política de contraseñas", "minimum-password-length": "Longitud mínima de contraseña", "minimum-password-length-required": "Se requiere una longitud mínima de contraseña", "minimum-password-length-range": "La longitud mínima de la contraseña debe estar en un rango de 5 a 50", @@ -108,8 +151,74 @@ "general-policy": "Política general", "max-failed-login-attempts": "Número máximo de intentos fallidos de inicio de sesión, antes de que la cuenta esté bloqueada", "minimum-max-failed-login-attempts-range": "El número máximo de intentos fallidos de inicio de sesión no puede ser negativo", - "user-lockout-notification-email": "En caso de bloqueo de la cuenta del usuario, envíe una notificación por correo electrónico" - }, + "user-lockout-notification-email": "En caso de bloqueo de la cuenta del usuario, envíe una notificación por correo electrónico", + "domain-name": "Nombre de dominio", + "domain-name-unique": "El nombre de dominio y protocolo debe ser único.", + "error-verification-url": "Un nombre de dominio no debe contener símbolos '/' y ':'. Ejemplo: thingsboard.io", + "oauth2": { + "access-token-uri": "URI Access token", + "access-token-uri-required": "Se requere URI Access token.", + "activate-user": "Activar usuario", + "add-domain": "Añadir dominio", + "delete-domain": "Borrar dominio", + "add-provider": "Añadir proveedor", + "delete-provider": "Borrar proveedor", + "allow-user-creation": "Permitir creación de usuario", + "always-fullscreen": "Siempre pantalla completa", + "authorization-uri": "URI Autorización", + "authorization-uri-required": "Se requiere URI de Autorización.", + "client-authentication-method": "Método de autenticación", + "client-id": "ID Cliente", + "client-id-required": "Se requere ID Cliente.", + "client-secret": "Secreto de Cliente", + "client-secret-required": "Se requiere Secreto de Cliente.", + "custom-setting": "Ajustes personalizados", + "customer-name-pattern": "Patrón nombre de cliente", + "default-dashboard-name": "Nombre de panel por defecto", + "delete-domain-text": "Atención, tras la confirmación el dominio y todos los datos del proveedor no estarán disponibles.", + "delete-domain-title": "Eliminar los ajustes del dominio '{{domainName}}'?", + "delete-registration-text": "Atención, tras la confirmación los datos del proveedor no estarán disponibles.", + "delete-registration-title": "Eliminar el proveedor '{{name}}'?", + "email-attribute-key": "Clave de atributos email", + "email-attribute-key-required": "Se requiere clave de atributos de email.", + "first-name-attribute-key": "Clave de atributos de nombre", + "general": "General", + "jwk-set-uri": "URI web key JSON", + "last-name-attribute-key": "Clave de atributos de apellido", + "login-button-icon": "Icono de botón login", + "login-button-label": "Etiqueta de proveedor", + "login-button-label-placeholder": "Login con $(Provider label)", + "login-button-label-required": "Clave de etiqueta requerida.", + "login-provider": "Proveedor de login", + "mapper": "Mapeador", + "new-domain": "Nuevo dominio", + "oauth2": "OAuth2", + "redirect-uri-template": "Plantilla de redirección URI", + "copy-redirect-uri": "Copiar URI de redirección", + "registration-id": "ID de registro", + "registration-id-required": "Se requiere ID de registro.", + "registration-id-unique": "El ID de registro debe ser único en el sistema.", + "scope": "Alcance", + "scope-required": "Se requiere alcance.", + "tenant-name-pattern": "Patrón de nombre de propietario", + "tenant-name-pattern-required": "Se requiere patrón de nombre de propietario.", + "tenant-name-strategy": "Estrategia de Nombre de Propietario", + "type": "Tipo de mapeador", + "uri-pattern-error": "Formato de URI inválido.", + "url": "URL", + "url-pattern": "Formato URL inválido.", + "url-required": "Se requiere URL.", + "user-info-uri": "URI Información de usuario", + "user-info-uri-required": "Se requiere URI de información usuario.", + "user-name-attribute-name": "Clave de atributos de nombre de usuario", + "user-name-attribute-name-required": "Se requiere clave de atributos de nombre de usuario", + "protocol": "Protocolo", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Activar ajustes OAuth2" + } + }, "alarm": { "alarm": "Alarma", "alarms": "Alarmas", @@ -117,6 +226,8 @@ "no-alarms-matching": "No se han encontrado alarmas coincidentes con '{{entity}}' .", "alarm-required": "Alarma requerida", "alarm-status": "Estado de Alarma", + "alarm-status-list": "Lista de estados de Alarmas", + "any-status": "Cualquier estado", "search-status": { "ANY": "Todas", "ACTIVE": "Activas", @@ -143,6 +254,8 @@ "end-time": "Hora fin", "ack-time": "Hora de reconocimiento", "clear-time": "Hora de normalización", + "alarm-severity-list": "Lista de gravedad de alarmas", + "any-severity": "Cualquier gravedad", "severity-critical": "Crítica", "severity-major": "Mayor", "severity-minor": "Menor", @@ -158,19 +271,23 @@ "min-polling-interval-message": "El ciclo debe ser por lo menos de 1 segundo.", "aknowledge-alarms-title": "Reconocer { count, plural, 1 {1 alarma} other {# alarmas} }", "aknowledge-alarms-text": "Estas seguro de reconocer { count, plural, 1 {1 alarma} other {# alarmas} }?", - "aknowledge-alarm-title": "Recononcer Alarma", + "aknowledge-alarm-title": "Recononcer Alarma", "aknowledge-alarm-text": "Estas seguro de reconocer Alarma?", "clear-alarms-title": "Normalizar { count, plural, 1 {1 alarma} other {# alarmas} }", - "clear-alarms-text": "Estás seguro de limpiar { count, plural, 1 {1 alarma} other {# alarmas} }?", - "clear-alarm-title": "Limpiar Alarma", - "clear-alarm-text": "Estás seguro de limpiar Alarma?", - "alarm-status-filter": "Filtro de Alarmas", + "clear-alarms-text": "Limpiar { count, plural, 1 {1 alarma} other {# alarmas} }?", + "clear-alarm-title": "Limpiar Alarma", + "clear-alarm-text": "Limpiar Alarma?", + "alarm-status-filter": "Filtro de estados de Alarmas", + "alarm-filter": "Filtro de Alarmas", "max-count-load": "Número máximo de alarmas a cargar (0 - ilimitado)", "max-count-load-required": "Se requiere número máximo de alarmas.", "max-count-load-error-min": "El valor mínimo es 0.", "fetch-size": "Tamaño de búsqueda (Fetch)", "fetch-size-required": "Se requiere tamaño de búsqueda.", - "fetch-size-error-min": "El valor mínimo es 10." + "fetch-size-error-min": "El valor mínimo es 10.", + "alarm-type-list": "Lista de tipos de alarma", + "any-type": "Cualquier tipo", + "search-propagated-alarms": "Buscar alarmas propagadas" }, "alias": { "add": "Añadir alias", @@ -200,6 +317,7 @@ "filter-type-device-search-query-description": "Dispositivos con tipos {{deviceTypes}} que tienen {{relationType}} relación {{direction}} {{rootEntity}}", "filter-type-entity-view-search-query": "Consulta de búsqueda de vista de entidad", "filter-type-entity-view-search-query-description": "Vistas de entidad con tipos {{entityViewTypes}} que tienen tipo de relación {{relationType}} con dirección {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "Uso de API", "entity-filter": "Filtro por entidad", "resolve-multiple": "Tomar como múltiples entidades", "filter-type": "Filtro por tipo", @@ -259,19 +377,19 @@ "unassign-assets": "Cancelar asignación de activo", "unassign-assets-action-title": "Cancelar asignación de { count, plural, 1 {1 activo} other {# activos} } del cliente", "assign-new-asset": "Asignar nuevo activo", - "delete-asset-title": "Estás seguro de borrar el activo '{{assetName}}'?", + "delete-asset-title": "Eliminar el activo '{{assetName}}'?", "delete-asset-text": "Atención, tras la confirmación el activo y sus datos serán borrados e irrecuperables.", - "delete-assets-title": "Estás seguro de borrar los activos { count, plural, 1 {1 activo} other {# activos} }?", + "delete-assets-title": "Eliminar los activos { count, plural, 1 {1 activo} other {# activos} }?", "delete-assets-action-title": "Borrar { count, plural, 1 {1 activo} other {# activos} }", "delete-assets-text": "Atención, tras la confirmación todos los activos seleccionados y sus datos serán borrados e irrecuperables.", - "make-public-asset-title": "Estás seguro de hacer el activo '{{assetName}}' público?", + "make-public-asset-title": "Hacer el activo '{{assetName}}' público?", "make-public-asset-text": "Tras la confirmación, el activo y sus datos se harán públicos y accesibles por otros.", - "make-private-asset-title": "Estás seguro de hacer el activo '{{assetName}}' privado?", + "make-private-asset-title": "Hacer el activo '{{assetName}}' privado?", "make-private-asset-text": "Tras la confirmación, el activo y sus datos se harán privados y no serán accesibles por otros.", - "unassign-asset-title": "Estás seguro de cancelar la asignación del activo '{{assetName}}'?", + "unassign-asset-title": "Cancelar la asignación del activo '{{assetName}}'?", "unassign-asset-text": "Tras la confirmación, el activo será desasignado y no será accesible por el cliente.", "unassign-asset": "Cancelar asignación de activo", - "unassign-assets-title": "Estás seguro de cancelar las asignaciones { count, plural, 1 {1 activo} other {# activos} }?", + "unassign-assets-title": "Cancelar las asignaciones { count, plural, 1 {1 activo} other {# activos} }?", "unassign-assets-text": "Tras la confirmación todos los activos seleccionados serán desasignados y no serán accesibles por el cliente.", "copyId": "Copiar ID de activo", "idCopiedMessage": "El ID ha sido copiado al portapapeles", @@ -281,6 +399,8 @@ "name-starts-with": "El nombre de activo comienza con", "import": "Importar activos", "asset-file": "Archivo del activo", + "search": "Buscar activos", + "selected-assets": "{ count, plural, 1 {1 activo} other {# activos} } seleccionados", "label": "Etiqueta" }, "attribute": { @@ -297,7 +417,7 @@ "key-required": "Clave del atributo requerida.", "value": "Valor", "value-required": "Valor del atributo requerido.", - "delete-attributes-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 atributo} other {# atributos} }?", + "delete-attributes-title": "¿Eliminar { count, plural, 1 {1 atributo} other {# atributos} }?", "delete-attributes-text": "Atención, tras la confirmación el atributo será eliminado, y la información relacionada será irrecuperable.", "delete-attributes": "Borrar atributo", "enter-attribute-value": "Ingresar valor del atributo", @@ -308,7 +428,62 @@ "add-to-dashboard": "Agregar al Panel", "add-widget-to-dashboard": "Agregar widget al Panel", "selected-attributes": "{ count, plural, 1 {1 atributo} other {# atributos} } seleccionados", - "selected-telemetry": "{ count, plural, 1 {1 telemetría} other {# telemetrías} } seleccionadas" + "selected-telemetry": "{ count, plural, 1 {1 telemetría} other {# telemetrías} } seleccionadas", + "no-attributes-text": "No se encontró ningún atributo", + "no-telemetry-text": "No se encontró ninguna telemetría" + }, + "api-usage": { + "api-usage": "Uso de API", + "data-points": "Puntos de datos", + "data-points-storage-days": "Días de grabación de puntos de datos", + "email": "Email", + "email-messages": "Mensajes de Email", + "email-messages-daily-activity": "Actividad diaria de Emails", + "email-messages-hourly-activity": "Actividad horaria de Emails", + "email-messages-monthly-activity": "Actividad mensual de Emails", + "exceptions": "Excepciones", + "executions": "Ejecuciones", + "javascript": "JavaScript", + "javascript-executions": "Ejecuciones JavaScript", + "javascript-functions": "Funciones JavaScript", + "javascript-functions-daily-activity": "Actividad diaria de funciones JavaScript", + "javascript-functions-hourly-activity": "Actividad horaria de funciones JavaScript", + "javascript-functions-monthly-activity": "Actividad mensual de funciones JavaScript", + "latest-error": "Último error", + "messages": "Mensajes", + "permanent-failures": "${entityName} Fallos permanentes", + "permanent-timeouts": "${entityName} Timeouts permanentes", + "processing-failures": "${entityName} Fallos de procesamiento", + "processing-failures-and-timeouts": "Fallos de procesamiento y timeouts", + "processing-timeouts": "${entityName} Timeouts de procesamiento", + "queue-stats": "Estadísticas de colas", + "rule-chain": "Cadena de reglas", + "rule-engine": "Motor de reglas", + "rule-engine-daily-activity": "Actividad diaria de motor de reglas", + "rule-engine-executions": "Ejecuciones de motor de reglas", + "rule-engine-hourly-activity": "Actividad horaria de motor de reglas", + "rule-engine-monthly-activity": "Actividad mensual de motor de reglas", + "rule-engine-statistics": "Estadisticas del motor de reglas", + "rule-node": "Nodo de reglas", + "sms": "SMS", + "sms-messages": "Mensajes SMS", + "sms-messages-daily-activity": "Actividad diaria de mensajes SMS", + "sms-messages-hourly-activity": "Actividad horaria de mensajes SMS", + "sms-messages-monthly-activity": "Actividad mensual de mensajes SMS", + "successful": "${entityName} Exitoso", + "telemetry": "Telemetría", + "telemetry-persistence": "Persistencia de telemetría", + "telemetry-persistence-daily-activity": "Actividad diaria de persistencia de telemetría", + "telemetry-persistence-hourly-activity": "Actividad horaria de persistencia de telemetría", + "telemetry-persistence-monthly-activity": "Actividad mensual de persistencia de telemetría", + "transport": "Transporte", + "transport-daily-activity": "Actividad diaria de transporte", + "transport-data-points": "Puntos de datos de transporte", + "transport-hourly-activity": "Actividad horaria de transporte", + "transport-messages": "Mensajes de transporte", + "transport-monthly-activity": "Actividad mensual de transporte", + "view-details": "Ver detalles", + "view-statistics": "Ver estadísticas" }, "audit-log": { "audit": "Auditoría", @@ -348,11 +523,17 @@ "action-data": "Datos de acción", "failure-details": "Detalles del error", "search": "Buscar registros de auditoría", - "clear-search": "Borrar búsqueda" + "clear-search": "Borrar búsqueda", + "type-assigned-from-tenant": "Asignado desde el administrador", + "type-assigned-to-tenant": "Asignado al administrador", + "type-provision-success": "Dispositivo aprovisionado", + "type-provision-failure": "Aprovisionamiento fallido", + "type-timeseries-updated": "Telemetría actualizada", + "type-timeseries-deleted": "Telemetría borrada" }, "confirm-on-exit": { - "message": "Tienes cambios sin guardar. ¿Estás seguro que quieres abandonar la página?", - "html-message": "Tienes cambios sin guardar.
¿Estás seguro que quieres abandonar la página?", + "message": "Tienes cambios sin guardar. ¿Abandonar la página?", + "html-message": "Tienes cambios sin guardar.
¿Abandonar la página?", "title": "Cambios sin guardar" }, "contact": { @@ -372,7 +553,9 @@ "password": "Contraseña", "enter-username": "Introduce el nombre de usuario.", "enter-password": "Introduce la contraseña", - "enter-search": "Introduce búsqueda" + "enter-search": "Introduce búsqueda", + "created-time": "Fecha de creación", + "loading": "Cargando..." }, "content-type": { "json": "Json", @@ -404,9 +587,9 @@ "add-customer-text": "Agregar nuevo cliente", "no-customers-text": "No se encontraron clientes", "customer-details": "Detalles del cliente", - "delete-customer-title": "¿Estás seguro que quieres eliminar el cliente '{{customerTitle}}'?", + "delete-customer-title": "¿Eliminar el cliente '{{customerTitle}}'?", "delete-customer-text": "Atención, tras la confirmación el cliente será eliminado y toda la información relacionada será irrecuperable.", - "delete-customers-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 cliente} other {# clientes} }?", + "delete-customers-title": "¿Eliminar { count, plural, 1 {1 cliente} other {# clientes} }?", "delete-customers-action-title": "Borrar { count, plural, 1 {1 cliente} other {# clientes} }", "delete-customers-text": "Atención, tras la confirmación todos los clientes seleccionados serán eliminados y su información relacionada será irrecuperable.", "manage-users": "Gestionar usuarios", @@ -425,7 +608,9 @@ "customer-required": "Cliente requerido", "select-default-customer": "Seleccionar cliente por defecto", "default-customer": "Cliente por defecto", - "default-customer-required": "Se requiere cliente por defecto para realizar debug a nivel de propietario" + "default-customer-required": "Se requiere cliente por defecto para realizar debu a nivel de propietario", + "search": "Buscar clientes", + "selected-customers": "{ count, plural, 1 {1 cliente} other {# clientes} } seleccionados" }, "datetime": { "date-from": "Fecha desde", @@ -471,20 +656,20 @@ "delete-dashboards": "Eliminar paneles", "unassign-dashboards": "Desasignar paneles", "unassign-dashboards-action-title": "Desasignar { count, plural, 1 {1 paneles} other {# paneles} } del cliente", - "delete-dashboard-title": "¿Estás seguro que quieres eliminar el panel '{{dashboardTitle}}'?", + "delete-dashboard-title": "¿Eliminar el panel '{{dashboardTitle}}'?", "delete-dashboard-text": "Atención, el panel seleccionado será eliminado y la información relacionada sera irrecuperable.", - "delete-dashboards-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 panel} other {# paneles} }?", + "delete-dashboards-title": "¿Eliminar { count, plural, 1 {1 panel} other {# paneles} }?", "delete-dashboards-action-title": "Eliminar { count, plural, 1 {1 panel} other {# paneles} }", "delete-dashboards-text": "Atención, los paneles seleccionados serán eliminados y la información relacionada será irrecuperable.", - "unassign-dashboard-title": "¿Estás seguro que quieres desasignar el panel '{{dashboardTitle}}'?", + "unassign-dashboard-title": "¿Desasignar el panel '{{dashboardTitle}}'?", "unassign-dashboard-text": "Tras la confirmación, el panel será desasignado y no podrá ser accesible por el cliente.", "unassign-dashboard": "Desasignar panel", - "unassign-dashboards-title": "¿Estás seguro que quieres desasignar { count, plural, 1 {1 panel} other {# paneles} }?", + "unassign-dashboards-title": "¿Desasignar { count, plural, 1 {1 panel} other {# paneles} }?", "unassign-dashboards-text": "Atención, tras la confirmación los paneles seleccionados serán desasignados y no podrán ser accesibles por el cliente.", "public-dashboard-title": "El panel ahora es público", "public-dashboard-text": "Tu panel {{dashboardTitle}} es ahora público y podrá ser accedido desde: aquí:", "public-dashboard-notice": "Nota: No olvides hacer públicos los dispositivos relacionados para acceder a sus datos.", - "make-private-dashboard-title": "¿Estás seguro que quieres hacer el panel '{{dashboardTitle}}' privado?", + "make-private-dashboard-title": "¿Hacer el panel '{{dashboardTitle}}' privado?", "make-private-dashboard-text": "Tras la confirmación, el panel será privado y no podrá ser accesible por otros.", "make-private-dashboard": "Hacer panel privado", "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", @@ -508,6 +693,9 @@ "min-columns-count-message": "Solo se permite un número mínimo de 10 columnas.", "max-columns-count-message": "Solo se permite un número máximo de 1000 columnas.", "widgets-margins": "Margen entre widgets", + "margin-required": "Valor de margen requerido.", + "min-margin-message": "0 es el valor de margen mínimo permitido.", + "max-margin-message": "50 es el valor de margen máximo permitido.", "horizontal-margin": "Margen horizontal", "horizontal-margin-required": "Margen horizontal requerido.", "min-horizontal-margin-message": "Solo se permite margen horizontal mínimo de 0.", @@ -527,6 +715,7 @@ "title-color": "Color del título", "display-dashboards-selection": "Mostrar selección de paneles", "display-entities-selection": "Mostrar selección de entidades", + "display-filters": "Mostrar filtros", "display-dashboard-timewindow": "Mostrar ventana de tiempo", "display-dashboard-export": "Mostrar exportar", "import": "Importar panel", @@ -560,6 +749,7 @@ "edit-state": "Editar estado panel", "delete-state": "Borrar estado panel", "add-state": "Añadir estado panel", + "no-states-text": "No se han encontrado estados", "state": "Estado de panel", "state-name": "Nombre", "state-name-required": "Se requiere nombre del estado.", @@ -568,11 +758,13 @@ "state-id-exists": "Ya existe un ID de estado.", "is-root-state": "Estado raiz(Root)", "delete-state-title": "Borrar estado de panel", - "delete-state-text": "Estás seguro de eliminar el estado de panel con nombre: '{{stateName}}'?", + "delete-state-text": "Eliminar el estado de panel con nombre: '{{stateName}}'?", "show-details": "Mostrar detalles", "hide-details": "Ocultar detalles", "select-state": "Seleccionar estado destino (target state)", - "state-controller": "Controlador de estados" + "state-controller": "Controlador de estados", + "search": "Buscar paneles", + "selected-dashboards": "{ count, plural, 1 {1 panel} other {# paneles} } seleccionados" }, "datakey": { "settings": "Ajustes", @@ -590,6 +782,7 @@ "alarm": "Campos de alarma", "timeseries-required": "Series de tiempo del dispositivo requerido.", "timeseries-or-attributes-required": "Series de tiempo/Atributos requeridos.", + "alarm-fields-timeseries-or-attributes-required": "Se requieren campos de alarma o series de tiempo/atributos.", "maximum-timeseries-or-attributes": "Máximo { count, plural, 1 {1 timeseries/atributo es permitido.} other {# timeseries/atributos son permitidos} }", "alarm-fields-required": "Campos de alarma requeridos.", "function-types": "Tipos de funciones", @@ -607,6 +800,7 @@ "add-datasource-prompt": "Por favor, agrega una fuente de datos" }, "details": { + "details": "Detalles", "edit-mode": "Modo Edición", "edit-json": "Editar JSON", "toggle-edit-mode": "Ir a Modo Edición" @@ -658,20 +852,20 @@ "unassign-devices": "Desasignar dispositivos", "unassign-devices-action-title": "Desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } del cliente", "assign-new-device": "Asignar nuevo dispositivo", - "make-public-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' público?", + "make-public-device-title": "¿Hacer el dispositivo '{{deviceName}}' público?", "make-public-device-text": "Tras la confirmación, el dispositivo y la información relacionada serán públicos y podrá ser accesible por otros.", - "make-private-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' privado?", + "make-private-device-title": "¿Hacer el dispositivo '{{deviceName}}' privado?", "make-private-device-text": "Tras la confirmación, el dispositivo y la información relacionada serán privados y no podrá ser accesible por otros.", "view-credentials": "Ver credenciales", - "delete-device-title": "¿Estás seguro que quieres eliminar el dispositivo '{{deviceName}}'?", + "delete-device-title": "¿Eliminar el dispositivo '{{deviceName}}'?", "delete-device-text": "Atención, tras la confirmación los dispositivos serán eliminados y la información relacionada será irrecuperable.", - "delete-devices-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", + "delete-devices-title": "¿Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", "delete-devices-action-title": "Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }", "delete-devices-text": "Atención, tras la confirmación los dispositivos seleccionados serán eliminados y la información relacionada será irrecuperable.", - "unassign-device-title": "¿Estás seguro que quieres desasignar el dispositivo '{{deviceName}}'?", + "unassign-device-title": "¿Desasignar el dispositivo '{{deviceName}}'?", "unassign-device-text": "Tras la confirmación, el dispositivo será desasignado y no podrá ser accesible por el cliente.", "unassign-device": "Desasignar dispositivo", - "unassign-devices-title": "¿Estás seguro que quieres desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", + "unassign-devices-title": "¿Desasignar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", "unassign-devices-text": "Tras la confirmación, los dispositivos seleccionados serán desasignados y no podrán ser accedidos por el cliente.", "device-credentials": "Credenciales del dispositivo", "credentials-type": "Tipo de credencial", @@ -680,6 +874,12 @@ "access-token-invalid": "Access token debe tener entre 1 a 20 caracteres.", "rsa-key": "Clave pública RSA", "rsa-key-required": "Clave pública RSA requerida.", + "client-id": "ID Cliente", + "client-id-pattern": "Contiene carácter inválido.", + "user-name": "Nombre Usuario", + "user-name-required": "Se requiere nombre de usuario.", + "client-id-or-user-name-necessary": "El ID Cliente y/o el Nombre de usuario son necesarios", + "password": "Contraseña", "secret": "Secreta", "secret-required": "Secreta requerida.", "device-type": "Tipo de dispositivo", @@ -692,25 +892,191 @@ "device-types": "Tipos de dispositivo", "name": "Nombre", "name-required": "El nombre es requerido.", - "label": "Etiqueta", "description": "Descripción", + "label": "Etiqueta", "events": "Eventos", "details": "Detalles", "copyId": "Copiar ID", "copyAccessToken": "Copiar access token", + "copy-mqtt-authentication": "Copiar credenciales MQTT", "idCopiedMessage": "Id del dispositivo copiado al portapapeles", "accessTokenCopiedMessage": "Access token del dispositivo copiado al portapapeles", + "mqtt-authentication-copied-message": "Los datos de autenticación MQTT se han copiado al portapapeles", "assignedToCustomer": "Asignado al cliente", "unable-delete-device-alias-title": "Imposible eliminar alias del dispositivo", "unable-delete-device-alias-text": "Alias '{{deviceAlias}}' no puede ser eliminado. Esta siendo usado por el(los) widget(s):
{{widgetsList}}", "is-gateway": "Es gateway", + "overwrite-activity-time": "Sobreescribir hora de actividad para el dispositivo conectado", "public": "Público", "device-public": "El dispositivo es público", "select-device": "Seleccionar dispositivo", + "import": "Importar dispositivo", "device-file": "Archivo de dispositivo", - "import": "Importar dispositivo" + "search": "Buscar dispositivos", + "selected-devices": "{ count, plural, 1 {1 dispositivo} other {# dispositivos} } seleccionados", + "device-configuration": "Configuración del dispositivo", + "transport-configuration": "Configuración del transporte", + "wizard": { + "device-wizard": "Asistente de dispositivo", + "device-details": "Detalles del dispositivo", + "new-device-profile": "Crear un nuevo perfil de dispositivo", + "existing-device-profile": "Seleccionar un perfil existente", + "specific-configuration": "Configuración específica", + "customer-to-assign-device": "Cliente al que asignar el dispositivo", + "add-credential": "Añadir credencial" + } + }, + "device-profile": { + "device-profile": "Perfil de dispositivo", + "device-profiles": "Perfiles de dispositivo", + "all-device-profiles": "Todos", + "add": "Añadir perfil de dispositivo", + "edit": "Editar perfil de dispositivo", + "device-profile-details": "Detalles de perfil de dispositivo", + "no-device-profiles-text": "No se encontraron perfiles", + "search": "Buscar perfiles", + "selected-device-profiles": "{ count, plural, 1 {1 perfil} other {# perfiles} } seleccionados", + "no-device-profiles-matching": "No existe perfil que conincida con '{{entity}}'.", + "device-profile-required": "Se requiere perfil de dispositivo", + "idCopiedMessage": "Se ha copiado el ID de perfil al portapapeles", + "set-default": "Hacer perfil por defecto", + "delete": "Borrar perfil de dispositivo", + "copyId": "Copiar ID de perfil", + "new-device-profile-name": "Nombre de perfil", + "new-device-profile-name-required": "Se requiere nombre de perfil.", + "name": "Nombre", + "name-required": "Se requiere nombre.", + "type": "Tipo de perfil", + "type-required": "Se requiere tipo de perfil.", + "type-default": "Por defecto", + "transport-type": "Tipo de transporte", + "transport-type-required": "Se requiere tipo de transporte.", + "transport-type-default": "Por defecto", + "transport-type-default-hint": "Soporta transportes por MQTT básico, HTTP y CoAP", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Activa ajustes avanzados de transporte MQTT", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "Transporte LWM2M", + "description": "Descripción", + "default": "Defecto", + "profile-configuration": "Configuración de perfil", + "transport-configuration": "Configuración de transporte", + "default-rule-chain": "Cadena de reglas por defecto", + "select-queue-hint": "Selecciona desde el desplegable o añade un nombre personalizado.", + "delete-device-profile-title": "Eliminar el perfil '{{deviceProfileName}}'?", + "delete-device-profile-text": "Atención, tras la confirmación el perfil y todos sus datos serán borrados e irrecuperables.", + "delete-device-profiles-title": "EEliminar { count, plural, 1 {1 perfil} other {# perfiles} }?", + "delete-device-profiles-text": "Atención, tras la confirmación los perfiles seleccionados y todos sus datos serán borrados e irrecuperables.", + "set-default-device-profile-title": "Establecer el perfil '{{deviceProfileName}}' como perfil por defecto?", + "set-default-device-profile-text": "Tras la confirmación, el perfil será marcado como por defecto y será usado por todos los nuevos dispositivos que no tengan perfil especificado.", + "no-device-profiles-found": "No se encontraron perfiles.", + "create-new-device-profile": "Crear un nuevo perfil!", + "mqtt-device-topic-filters": "Filtros de topic MQTT", + "mqtt-device-topic-filters-unique": "Los filtros de topic de dispositivo MQTT deben ser únicos.", + "mqtt-device-payload-type": "Payload de dispositivo MQTT", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Se requiere tipo de Payload.", + "support-level-wildcards": "Se soportan los wilcards únicos [+] y multi-nivel [#].", + "telemetry-topic-filter": "Filtro de topic en telemetría", + "telemetry-topic-filter-required": "Se requiere filtro de topic (telemetría).", + "attributes-topic-filter": "Filtro de topic en atributos", + "attributes-topic-filter-required": "Se requiere filtro de topic (atributos).", + "telemetry-proto-schema": "Proto schema de telemetría", + "telemetry-proto-schema-required": "Se requiere proto schema de telemetría.", + "attributes-proto-schema": "Proto schema de atributos", + "attributes-proto-schema-required": "Se requiere proto schema de atributos.", + "rpc-response-topic-filter": "Filtro de topic de respuesta RPC", + "rpc-response-topic-filter-required": "Se requiere fitro de respuesta RPC.", + "not-valid-pattern-topic-filter": "No es un patrón de filtro válido", + "not-valid-single-character": "Uso inválido de wildcard único", + "not-valid-multi-character": "Uso inválido de wildcard multi-nivel", + "single-level-wildcards-hint": "[+] es adecuado para cualquier nivel. Ej.: v1/devices/+/telemetry o +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] puede reemplazar el mismo filtro y debe ser el último símbolo del topic. Ej.: # o v1/devices/me/#.", + "alarm-rules": "Reglas de alarma", + "alarm-rules-with-count": "Reglas de alarma ({{count}})", + "no-alarm-rules": "No hay reglas de alarma configuradas", + "add-alarm-rule": "Añadir regla de alarma", + "edit-alarm-rule": "Editar regla de alarma", + "alarm-type": "Tipo de alarma", + "alarm-type-required": "Se requiere tipo de alarma.", + "alarm-type-unique": "El tipo de alarma, debe ser único dentro de las reglas de alarma del perfil de dispositivo.", + "create-alarm-pattern": "Crear alarma {{alarmType}}", + "create-alarm-rules": "Crear reglas de alarma", + "no-create-alarm-rules": "No hay condiciones de creación de alarma configuradas", + "add-create-alarm-rule-prompt": "Por favor, añade una regla de alarma", + "clear-alarm-rule": "Borrar regla de alarma", + "no-clear-alarm-rule": "No hay condiciones de borrado de alarma configuradas", + "add-create-alarm-rule": "Añadir crear condición (activar alarma)", + "add-clear-alarm-rule": "Añair borrar condición (limpiar alarma)", + "select-alarm-severity": "Selecciona severidad de alarma", + "alarm-severity-required": "Se requiere especificar severidad de alarma.", + "condition-duration": "Duración de condición", + "condition-duration-value": "Valor de duración", + "condition-duration-time-unit": "Unidad de tiempo", + "condition-duration-value-range": "El valor debe estar en un rango desde 1 a 2147483647.", + "condition-duration-value-pattern": "El valor de duración debe ser un número entero.", + "condition-duration-value-required": "Se requiere valor de duración.", + "condition-duration-time-unit-required": "Se requiere una unidad de tiempo.", + "advanced-settings": "Ajustes avanzados", + "alarm-rule-details": "Detalles", + "add-alarm-rule-details": "Añadir detalles", + "propagate-alarm": "Propagar alarma", + "alarm-rule-relation-types-list": "Tipos de relación para propagar", + "alarm-rule-relation-types-list-hint": "Si no está seleccionado 'propagar relaciones', las alarmas serán propagadas sin filtrar por relación.", + "alarm-details": "Detalles de alarma", + "alarm-rule-condition": "Condiciones de regla de alarma", + "enter-alarm-rule-condition-prompt": "Por favor, añade una condición de alarma", + "edit-alarm-rule-condition": "Editar condición de alarma", + "device-provisioning": "Aprovisionamiento de dispositivos", + "provision-strategy": "Estrategia de aprovisionamiento", + "provision-strategy-required": "Se requiere estrategia de aprovisionamiento.", + "provision-strategy-disabled": "Desactivado", + "provision-strategy-created-new": "Permitir crear nuevos dispositivos", + "provision-strategy-check-pre-provisioned": "Revisar dispositivos pre-aprovisionados", + "provision-device-key": "Clave de aprovisionamiento", + "provision-device-key-required": "Se requiere clave de aprovisionamiento.", + "copy-provision-key": "Copiar clave de aprovisionamiento", + "provision-key-copied-message": "La clave de aprovisionamiento se ha copiado al portapapeles", + "provision-device-secret": "Secreto de aprovisionamiento", + "provision-device-secret-required": "Se requiere secreto de aprovisionamiento.", + "copy-provision-secret": "Copiar secreto de aprovisionamiento", + "provision-secret-copied-message": "Se ha copiado el secreto de aprovisionamiento al portapapeles", + "condition": "Condición", + "condition-type": "Tipo de condición", + "condition-type-simple": "Simple", + "condition-type-duration": "Duración", + "condition-during": "Durante {{during}}", + "condition-type-repeating": "Repetitiva", + "condition-type-required": "Se requiere tipo de condición.", + "condition-repeating-value": "Nº de eventos", + "condition-repeating-value-range": "El Nº de eventos debe estar en un rango de 1 to 2147483647.", + "condition-repeating-value-pattern": "Nº de eventos debe ser un número entero.", + "condition-repeating-value-required": "Se requiere Nº de eventos.", + "condition-repeat-times": "Repetición { count, plural, 1 {1 vez} other {# veces} }", + "schedule-type": "Tipo de horario", + "schedule-type-required": "Tipo de horario requerido.", + "schedule": "Horario", + "edit-schedule": "Editar horario de alarma", + "schedule-any-time": "Siempre activo", + "schedule-specific-time": "Activo en una hora específica", + "schedule-custom": "Personalizado", + "schedule-day": { + "monday": "Lunes", + "tuesday": "Martes", + "wednesday": "Miércoles", + "thursday": "Jueves", + "friday": "Viernes", + "saturday": "Sábado", + "sunday": "Domingo" + }, + "schedule-days": "Días", + "schedule-time": "Hora", + "schedule-time-from": "De", + "schedule-time-to": "Hasta", + "schedule-days-of-week-required": "Debe ser seleccionado por lo menos un día de la semana." }, - "dialog": { + "dialog": { "close": "Cerrar diálogo" }, "direction": { @@ -766,6 +1132,10 @@ "type-devices": "Dispositivos", "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista de # Dispositivos} }", "device-name-starts-with": "Dispositivos cuyos nombres comiencen por '{{prefix}}'", + "type-device-profile": "Perfil de dispositivo", + "type-device-profiles": "Perfiles de dispositivo", + "list-of-device-profiles": "{ count, plural, 1 {un perfil} other {Lista de # perfiles} }", + "device-profile-name-starts-with": "Perfiles cuyo nombre empiece por '{{prefix}}'", "type-asset": "Activo", "type-assets": "Activos", "list-of-assets": "{ count, plural, 1 {Un activo} other {Lista de # activos} }", @@ -785,7 +1155,11 @@ "type-tenant": "Propietario", "type-tenants": "Propietarios", "list-of-tenants": "{ count, plural, 1 {Un propietario} other {Lista de # propietarios} }", - "tenant-name-starts-with": "Tenants cuyos nombres comiencen por '{{prefix}}'", + "tenant-name-starts-with": "Propietarios cuyo nombre comience por '{{prefix}}'", + "type-tenant-profile": "Perfil de Propietario", + "type-tenant-profiles": "Perfiles de propietario", + "list-of-tenant-profiles": "{ count, plural, 1 {Un perfil de propietario} other {Lista de # perfiles de propietario} }", + "tenant-profile-name-starts-with": "Pefiles de propietario cuyo nombre empiece por '{{prefix}}'", "type-customer": "Cliente", "type-customers": "Clientes", "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista de # clientes} }", @@ -812,6 +1186,8 @@ "rulenode-name-starts-with": "Nodos de reglas cuyos nombres comienzan con '{{prefix}}'", "type-current-customer": "Cliente Actual", "type-current-tenant": "Propietario Actual", + "type-current-user": "Usuario Actual", + "type-current-user-owner": "Usuario Propietario Actual", "search": "Buscar entidades", "selected-entities": "{ count, plural, 1 {1 entidad} other {# entidades} } seleccionadas", "entity-label": "Etiqueta de entidad", @@ -819,10 +1195,11 @@ "details": "Detalles de entidad", "no-entities-prompt": "No se han encontrado entidades", "no-data": "No hay datos que mostrar", - "columns-to-display": "Columnas a Mostrar" + "columns-to-display": "Columnas a Mostrar", + "type-api-usage-state": "Estado de uso de la API" }, "entity-field": { - "created-time": "Tiempo de creación", + "created-time": "Hora de creación", "name": "Nombre", "type": "Tipo", "first-name": "Nombre", @@ -855,6 +1232,7 @@ "duplicate-alias-error": "Alias duplicado'{{alias}}'.
Los alias de Entity View deben ser únicos en el panel.", "configure-alias": "Configurar alias '{{alias}}'", "no-entity-views-matching": "No se encontraron vistas que coincidan con '{{entity}}'.", + "public": "Público", "alias": "Alias", "alias-required": "Alias de vista de entidad es requerido.", "remove-alias": "Borrar alias de la vista de entidad", @@ -866,6 +1244,7 @@ "entity-view-name-filter-required": "Nombre del filtro de vista de entidad es requerido.", "entity-view-name-filter-no-entity-view-matched": "No se encontraron vistas de entidad que comiencen con '{{entityView}}'.", "add": "Añadir vista de entidad", + "entity-view-public": "Vista de entidad es pública", "assign-to-customer": "Asignar a cliente", "assign-entity-view-to-customer": "Asignar vista de entidad a cliente", "assign-entity-view-to-customer-text": "Por favor, seleccione las vistas de entidad para asignar al cliente", @@ -882,15 +1261,15 @@ "unassign-entity-views-action-title": "Anular asignación { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } al cliente", "assign-new-entity-view": "Asignar nueva vista de entidad", "delete-entity-view-title": "¿Está seguro que quiere borrar la vista de entidad '{{entityViewName}}'?", - "delete-entity-view-text": "¡Cuidado! Después de la confirmación, la vista de la entidad y todos los datos relacionados serán irrecuperables.", + "delete-entity-view-text": "¡Cuidado! Tras la confirmación, la vista de la entidad y todos los datos relacionados serán irrecuperables.", "delete-entity-views-title": "¿Está seguro que quiere borrar las vistas de entidad { count, plural, 1 {1 entityView} other {# entityViews} }?", "delete-entity-views-action-title": "Borrar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }", - "delete-entity-views-text": "¡Cuidado! Después de la confirmación, todas las vistas de entidades seleccionadas se eliminarán y todos los datos relacionados serán irrecuperables.", + "delete-entity-views-text": "¡Cuidado! Tras la confirmación, todas las vistas de entidades seleccionadas se eliminarán y todos los datos relacionados serán irrecuperables.", "unassign-entity-view-title": "¿Está seguro que quiere anular la asignación de la vista de entidad '{{entityViewName}}'?", - "unassign-entity-view-text": "Después de la confirmación, la vista de la entidad quedará sin asignar y el cliente no podrá acceder a ella.", + "unassign-entity-view-text": "Tras la confirmación, la vista de la entidad quedará sin asignar y el cliente no podrá acceder a ella.", "unassign-entity-view": "Anular asignación de la vista de entidad", "unassign-entity-views-title": "¿Está seguro que quiere anular la asignación de { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?", - "unassign-entity-views-text": "Después de la confirmación, todas las vistas de entidades seleccionadas quedarán sin asignar y el cliente no podrá acceder a ellas.", + "unassign-entity-views-text": "Tras la confirmación, todas las vistas de entidades seleccionadas quedarán sin asignar y el cliente no podrá acceder a ellas.", "entity-view-type": "Tipo de vista de entidad", "entity-view-type-required": "Tipo de vista de entidad es requerido.", "select-entity-view-type": "Seleccione el tipo de vista de entidad", @@ -899,12 +1278,14 @@ "no-entity-view-types-matching": "No se encontraron tipos de vista de entidad que coincidan con '{{entitySubtype}}'.", "entity-view-type-list-empty": "No hay tipos de vista de entidad seleccionados.", "entity-view-types": "Tipos de vista de entidad", + "created-time": "Fecha de creación", "name": "Nombre", "name-required": "Nombre Requerido.", "description": "Descripción", "events": "Eventos", "details": "Detalles", "copyId": "Copiar el Id de la vista de entidad", + "idCopiedMessage": "El Id de la vista de entidad se ha copiado al portapapeles", "assignedToCustomer": "Asignado a cliente", "unable-entity-view-device-alias-title": "No se puede eliminar el alias de vista de entidad", "unable-entity-view-device-alias-text": "El alias del dispositivo '{{entityViewAlias}}' no se puede borrar porque está siendo usado por el widget(s):
{{widgetsList}}", @@ -930,9 +1311,11 @@ "timeseries-data": "Datos de series temporales", "timeseries-data-hint": "Configure las claves de los datos de las series temporales de la entidad de destino que serán accesibles para la vista de la entidad. Los datos de esta serie temporal son de solo lectura.", "make-public-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea pública?", - "make-public-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán públicos y accesibles para otros.", + "make-public-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán públicos y accesibles para otros.", "make-private-entity-view-title": "¿Está seguro de que desea que la vista de entidad '{{entityViewName}}' sea privada?", - "make-private-entity-view-text": "Después de la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros." + "make-private-entity-view-text": "Tras la confirmación, la vista de la entidad y todos sus datos se harán privados y no serán accesibles para otros.", + "search": "Buscar vistas de entidad", + "selected-entity-views": "{ count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } seleccionadas" }, "event": { "event-type": "Tipo de evento", @@ -965,7 +1348,7 @@ }, "extension": { "extensions": "Extensiones", - "selected-extensions": "{ count, plural, 1 {1 extension} other {# extensions} } seleccionadas", + "selected-extensions": "{ count, plural, 1 {1 extensión} other {# extensiones} } seleccionadas", "type": "Tipo", "key": "Clave", "value": "Valor", @@ -977,9 +1360,9 @@ "delete": "Borrar Extensión", "add": "Añadir Extensión", "edit": "Editar Extensión", - "delete-extension-title": "Estás seguro de borrar la extensión '{{extensionId}}'?", + "delete-extension-title": "Eliminar la extensión '{{extensionId}}'?", "delete-extension-text": "Atención, tras la confirmación la extensión y sus datos serán borrados e irrecuperables.", - "delete-extensions-title": "Estás seguro de borrar las extensiones { count, plural, 1 {1 extensión} other {# extensiones} }?", + "delete-extensions-title": "Eliminar las extensiones { count, plural, 1 {1 extensión} other {# extensiones} }?", "delete-extensions-text": "Atención, tras la confirmación todas las extensiones seleccionadas y sus datos serán borrados e irrecuperables.", "converters": "Convertidores", "converter-id": "Id de convertidor", @@ -1120,6 +1503,93 @@ "file": "Fichero de extensiones", "invalid-file-error": "Fichero de extensiones inválido" }, + "filter": { + "add": "Añadir filtro", + "edit": "Editar filtro", + "name": "Nombre de filtro", + "name-required": "Se requiere nombre de filtro.", + "duplicate-filter": "Ya existe un filtro con el mismo nombre.", + "filters": "Filtros", + "unable-delete-filter-title": "Error borrando filtro", + "unable-delete-filter-text": "El filtro '{{filter}}' no puede ser borrado debido a que está siendo usado actualmente por los siguientes widgets:
{{widgetsList}}", + "duplicate-filter-error": "Se ha encontrado un filtro duplicado '{{filter}}'.
Los filtros deben ser únicos en el panel.", + "missing-key-filters-error": "No se encontró la clave de filtros para el filtro '{{filter}}'.", + "filter": "Filtro", + "editable": "Editable", + "no-filters-found": "No se encontraron filtros.", + "no-filter-text": "No se ha especificado filtro", + "add-filter-prompt": "Por favos, añadir filtro", + "no-filter-matching": "'{{filter}}' no encontrado.", + "create-new-filter": "Crear un filtro nuevo!", + "filter-required": "Se requiere filtro.", + "operation": { + "operation": "Operación", + "equal": "igual", + "not-equal": "no igual", + "starts-with": "comienza con", + "ends-with": "acaba con", + "contains": "contiene", + "not-contains": "no contiene", + "greater": "mayor que", + "less": "menor que", + "greater-or-equal": "mayor o igual", + "less-or-equal": "menor o igual", + "and": "y", + "or": "o" + }, + "ignore-case": "Ignorar mayús/minus", + "value": "Valor", + "remove-filter": "Borrar filtro", + "preview": "Vista previa de filtro", + "no-filters": "No hay filtros configurados", + "add-filter": "Añadir filtro", + "add-complex-filter": "Añadir filtro complejo", + "add-complex": "Agregar filtro complejo", + "complex-filter": "Filtro complejo", + "edit-complex-filter": "Editar filtro complejo", + "edit-filter-user-params": "Editar parámetros de usuario del filtro", + "filter-user-params": "Filtro de parámetros de usuario (predicado)", + "user-parameters": "Parámetros de usuario", + "display-label": "Etiqueta a mostrar", + "autogenerated-label": "Auto generar etiqueta", + "order-priority": "Prioridad orden de campos", + "key-filter": "Filtros (clave)", + "key-filters": "Filtros (claves)", + "key-name": "Nombre de clave", + "key-name-required": "Se requiere nombre de clave.", + "key-type": { + "key-type": "Tipo de clave", + "attribute": "Atributo", + "timeseries": "Timeseries", + "entity-field": "Campo de entidad" + }, + "value-type": { + "value-type": "Tipo de valor", + "string": "Cadena", + "numeric": "Numerico", + "boolean": "Booleano", + "date-time": "Fecha/Hora" + }, + "value-type-required": "Se requiere tipo de valor.", + "key-value-type-change-title": "Cambiar el tipo de valor de la clave?", + "key-value-type-change-message": "Si confirmas el nuevo tipo, todos los filtros se borrarán.", + "no-key-filters": "No hay filtros claves configurados", + "add-key-filter": "Añadir filtro clave", + "remove-key-filter": "Borrar filtro clave", + "edit-key-filter": "Editar filtro clave", + "date": "Fecha", + "time": "Hora", + "current-tenant": "Admin actual", + "current-customer": "Cliente actual", + "current-user": "Usuario actual", + "current-device": "Dispositivo actual", + "default-value": "Valor por defecto", + "dynamic-source-type": "Tipo de origen dinámico", + "no-dynamic-value": "Sin valor dinámico", + "source-attribute": "Atributo de origen", + "switch-to-dynamic-value": "Cambiar a valor dinámico", + "switch-to-default-value": "Cambiar a valor por defecto" + }, "fullscreen": { "expand": "Expandir a Pantalla Completa", "exit": "Salir de Pantalla Completa", @@ -1139,7 +1609,7 @@ "connector-type-required": "Se requiere tipo conector.", "connectors": "Configuración de conectores", "create-new-gateway": "Crear un gateway nuevo", - "create-new-gateway-text": "Estás seguro de crear un nuevo gateway con el nombre: '{{gatewayName}}'?", + "create-new-gateway-text": "Crear un nuevo gateway con el nombre: '{{gatewayName}}'?", "delete": "Borrar configuración", "download-tip": "Descargar fichero de configuración", "gateway": "Gateway", @@ -1201,17 +1671,12 @@ "tls-path-private-key": "Ruta a la clave privada en el gateway", "toggle-fullscreen": "Pantalla completa fullscreen", "transformer-json-config": "Configuración JSON*", - "update-config": "Añadir/actualizar configuración JSON", - "state-title": "Estado gateway", - "show-config-tip": "Mostrar configuración gateway", - "title-show-config": "Mostrar configuración gateway", - "read-only": "Solo lectura", - "read-write": "" + "update-config": "Añadir/actualizar configuración JSON" }, "grid": { - "delete-item-title": "¿Estás seguro que quieres eliminar este item?", + "delete-item-title": "¿Quieres eliminar este item?", "delete-item-text": "Atención, tras la confirmación el item será eliminado y la información relacionada será irrecuperable.", - "delete-items-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 item} other {# items} }?", + "delete-items-title": "¿Quieres eliminar { count, plural, 1 {1 item} other {# items} }?", "delete-items-action-title": "Eliminar { count, plural, 1 {1 item} other {# items} }", "delete-items-text": "Atención, tras la confirmación los items seleccionados serán eliminados y la información relacionada será irrecuperable.", "add-item-text": "Agregar nuevo item", @@ -1235,10 +1700,10 @@ "import": { "no-file": "Ningún archivo seleccionado", "drop-file": "Suelte un archivo JSON o haga clic para seleccionar un archivo para cargar.", + "drop-file-csv": "Suelte un archivo CSV o haga clic para seleccionar un archivo para cargar.", "column-value": "Valor", "column-title": "Título", "column-example": "Datos de ejemplo", - "drop-file-csv": "Suelte un archivo CSV o haga clic para seleccionar un archivo para cargar.", "column-key": "Clave de atributo/telemetría", "csv-delimiter": "Delimitador CSV", "csv-first-line-header": "La primera línea contiene nombres de columna.", @@ -1257,6 +1722,7 @@ "entity-field": "Campo de entidad", "access-token": "Token de acceso", "isgateway": "Es Gateway", + "activity-time-from-gateway-device": "Fecha de actividad desde el dispositivo gateway", "description": "Descripción" }, "stepper-text": { @@ -1300,6 +1766,7 @@ "legend": { "direction": "Dirección de la leyenda", "position": "Posición de la leyenda", + "sort-legend": "Ordenar claves en leyenda", "show-max": "Mostrar valor máximo", "show-min": "Mostrar valor mínimo", "show-avg": "Mostrar valor promedio", @@ -1376,20 +1843,21 @@ "any-relation-type": "Cualquier tipo", "add": "Añadir relación", "edit": "Editar relación", - "delete-to-relation-title": "¿Estás seguro que quieres eliminar la relación con la entidad '{{entityName}}'?", + "delete-to-relation-title": "¿Quieres eliminar la relación con la entidad '{{entityName}}'?", "delete-to-relation-text": "Atención, tras la confirmación la entidad '{{entityName}}' no estará relacionada con la entidad actual.", - "delete-to-relations-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", + "delete-to-relations-title": "¿Quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", "delete-to-relations-text": "Atención, tras la confirmación todas las relaciones seleccionadas se eliminarán y sus entidades correspondientes no estarán relacionadas con la entidad actual.", - "delete-from-relation-title": "¿Estás seguro que quieres eliminar la relación con la entidad '{{entityName}}'?", + "delete-from-relation-title": "¿Quieres eliminar la relación con la entidad '{{entityName}}'?", "delete-from-relation-text": "Atención, tras la confirmación la entidad actual no estará relacionada con la entidad '{{entityName}}'.", - "delete-from-relations-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", + "delete-from-relations-title": "¿Quieres eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", "delete-from-relations-text": "Atención, tras la confirmación todas las relaciones seleccionadas se eliminarán y sus entidades correspondientes no estarán relacionadas con sus entidades correspondientes.", "remove-relation-filter": "Quitar filtro de relación", "add-relation-filter": "Añadir filtro de relación", "any-relation": "Cualquier relación", "relation-filters": "Filtro de relación", "additional-info": "Información adicional (JSON)", - "invalid-additional-info": "Error al analizar el fichero JSON de información adicional." + "invalid-additional-info": "Error al analizar el fichero JSON de información adicional.", + "no-relations-text": "No se encontraron relaciones" }, "rulechain": { "rulechain": "Cadena de Regla", @@ -1401,9 +1869,9 @@ "description": "Descripción", "add": "Añadir Cadena", "set-root": "Hacer la cadena de reglas Raíz", - "set-root-rulechain-title": "¿Estás seguro de que desea hacer la cadena de reglas '{{ruleChainName}}' de tipo raíz?", + "set-root-rulechain-title": "¿Desea hacer la cadena de reglas '{{ruleChainName}}' de tipo raíz?", "set-root-rulechain-text": "Tras la confirmación, la cadena de reglas se volverá raíz y manejará todos los mensajes de transporte entrantes.", - "delete-rulechain-title": "¿Estás seguro que quieres eliminar la cadena de reglas '{{ruleChainName}}'?", + "delete-rulechain-title": "¿Quieres eliminar la cadena de reglas '{{ruleChainName}}'?", "delete-rulechain-text": "Atención, tras la confirmación la cadena de reglas y todos los datos serán irrecuperables.", "delete-rulechains-title": "¿Está seguro que quieres eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }?", "delete-rulechains-action-title": "Eliminar { count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} }", @@ -1426,7 +1894,10 @@ "no-rulechains-matching": "No se encontraron cadenas de reglas que coincidan con '{{entity}}' .", "rulechain-required": "Cadena de reglas requerida", "management": "Gestión de reglas", - "debug-mode": "Modo Debug" + "debug-mode": "Modo Debug", + "search": "Buscar cadenas de reglas", + "selected-rulechains": "{ count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } seleccionadas", + "open-rulechain": "Abrir cadena de reglas" }, "rulenode": { "details": "Detalles", @@ -1478,9 +1949,9 @@ "type-unknown": "Desconocido", "type-unknown-details": "Regla de nodo no resuelta", "directive-is-not-loaded": "La directiva de configuración definida '{{directiveName}}' no está disponible.", - "ui-resources-load-error": "Error al cargar los recursos de configuración ui.", + "ui-resources-load-error": "Error al cargar los recursos de configuración UI.", "invalid-target-rulechain": "No se puede resolver la cadena de reglas objetivo!", - "test-script-function": "Probar Script Función", + "test-script-function": "Probar Script de función", "message": "Mensaje", "message-type": "Tipo de mensaje", "select-message-type": "Seleccionar tipo de mensaje", @@ -1492,11 +1963,16 @@ "help": "Ayuda", "reset-debug-mode": "Restablecer el modo de depuración en todos los nodos" }, + "timezone": { + "timezone": "Zona Horaria", + "select-timezone": "Seleccionar zona horaria", + "no-timezones-matching": "No hay zonas horarias que coincidan con '{{timezone}}'.", + "timezone-required": "Se requiere zona horaria." + }, "queue": { - "select_name": "Selecciona el nombre de la cola", - "name": "Nombre Cola", - "name_required": "Necesario especificar el nombre de cola" - + "select_name": "Selecciona el nombre de la cola", + "name": "Nombre Cola", + "name_required": "Necesario especificar el nombre de cola" }, "tenant": { "tenant": "Propietario", @@ -1509,9 +1985,9 @@ "add-tenant-text": "Agregar nuevo propietario", "no-tenants-text": "Ningún propietario encontrado", "tenant-details": "Detalles del propietario", - "delete-tenant-title": "¿Estás seguro que quieres eliminar el propietario '{{tenantTitle}}'?", + "delete-tenant-title": "¿Quieres eliminar el propietario '{{tenantTitle}}'?", "delete-tenant-text": "Atención, tras la confirmación el propietario será eliminado y la información relacionada será irrecuperable.", - "delete-tenants-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 propietario} other {# propietarios} }?", + "delete-tenants-title": "¿Quieres eliminar { count, plural, 1 {1 propietario} other {# propietarios} }?", "delete-tenants-action-title": "Eliminar { count, plural, 1 {1 propietario} other {# propietarios} }", "delete-tenants-text": "Atención, tras la confirmación los propietarios seleccionados serán eliminados y la información relacionada será irrecuperable.", "title": "Título", @@ -1524,21 +2000,110 @@ "select-tenant": "Seleccionar propietario", "no-tenants-matching": "No hay propietarios que coincidan con '{{entity}}' .", "tenant-required": "Propietario requerido", + "search": "Buscar propietarios", + "selected-tenants": "{ count, plural, 1 {1 propietario} other {# propietarios} } seleccionados", "isolated-tb-core": "Procesando en contenedor aislado", "isolated-tb-rule-engine": "Procesando en contenedor Motor de Reglas aislado", "isolated-tb-core-details": "Requiere microservicios separados por propietario aislado", "isolated-tb-rule-engine-details": "Requiere microservicios separados por propietario aislado" }, + "tenant-profile": { + "tenant-profile": "Perfil de propietario", + "tenant-profiles": "Perfiles de propietarios", + "add": "Añadir perfil de propietario", + "edit": "Editar perfil de propietario", + "tenant-profile-details": "Detalles perfil de propietario", + "no-tenant-profiles-text": "No se encontraron perfiles de propietario", + "search": "Buscar perfiles de propietario", + "selected-tenant-profiles": "{ count, plural, 1 {1 perfil de propietario} other {# perfiles de propietario} } seleccionados", + "no-tenant-profiles-matching": "No se han encontrado perfiles de propietario que coincidan con '{{entity}}'.", + "tenant-profile-required": "Se requiere perfil de propietario", + "idCopiedMessage": "El ID de perfil de propietario se ha copiado al portapapeles", + "set-default": "Hacer perfil propietario por defecto", + "delete": "Borrar perfil", + "copyId": "Copiar ID de perfil", + "name": "Nombre", + "name-required": "Se requiere nombre.", + "data": "Datos de perfil", + "profile-configuration": "Configuración de perfil", + "description": "Descripción", + "default": "Defecto", + "delete-tenant-profile-title": "Eliminar el perfil propietario '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Atención, tras la confirmación, el perfil de propietario será borrado y su información relacionada será irrecuperable.", + "delete-tenant-profiles-title": "Eliminar { count, plural, 1 {1 perfil propietario} other {# perfiles propietarios} }?", + "delete-tenant-profiles-text": "Atención, tras la confirmación, los perfiles seleccionados se eliminarán y su información relacionada será irrecuperable.", + "set-default-tenant-profile-title": "Quieres hacer el perfil propietario '{{tenantProfileName}}' por defecto?", + "set-default-tenant-profile-text": "Tras la confirmación, el perfil propietario será marcado por defecto y será usado por los nuevos perfiles propietarios que no tengan perfil específico.", + "no-tenant-profiles-found": "No se encontraron perfiles de propietario.", + "create-new-tenant-profile": "Crear un nuevo perfil!", + "maximum-devices": "Nº Máximo de dispositivos (0 - sin límite)", + "maximum-devices-required": "Nº Máximo de dispositivos requerido.", + "maximum-devices-range": "Nº Máximo de dispositivos no puede ser negativo", + "maximum-assets": "Nº Máximo de activos (0 - sin límite)", + "maximum-assets-required": "Nº Máximo de activos requerido.", + "maximum-assets-range": "Nº Máximo de activos no puede ser negativo", + "maximum-customers": "Nº Máximo de clientes (0 - sin límite)", + "maximum-customers-required": "Nº Máximo de clientes requerido.", + "maximum-customers-range": "Nº Máximo de clientes no puede ser negativo", + "maximum-users": "Nº Máximo de usuarios (0 - sin límite)", + "maximum-users-required": "Nº Máximo de usuarios requerido.", + "maximum-users-range": "Nº Máximo de usuarios no puede ser negativo", + "maximum-dashboards": "Nº Máximo de paneles (0 - sin límite)", + "maximum-dashboards-required": "Nº Máximo de paneles requerido.", + "maximum-dashboards-range": "Nº Máximo de paneles no puede ser negativo", + "maximum-rule-chains": "Nº Máximo de cadenas de reglas (0 - sin límite)", + "maximum-rule-chains-required": "Nº Máximo de cadenas de reglas requerido.", + "maximum-rule-chains-range": "Nº Máximo de cadenas de reglas no puede ser negativo", + "transport-tenant-msg-rate-limit": "Tasa de mensajes de transporte por propietario.", + "transport-tenant-telemetry-msg-rate-limit": "Tasa de mensajes de telemetría por propietario.", + "transport-tenant-telemetry-data-points-rate-limit": "Tasa de datapoints por propietario.", + "transport-device-msg-rate-limit": "Tasa de mensajes de dispositivo.", + "transport-device-telemetry-msg-rate-limit": "Tasa de mensajes de telemetría de dispositivo.", + "transport-device-telemetry-data-points-rate-limit": "Tasa de datapoints de telemetría de dispositivo.", + "max-transport-messages": "Nº Máximo de mensajes de transporte (0 - sin límite)", + "max-transport-messages-required": "Nº Máximo de mensajes de transporte requerido.", + "max-transport-messages-range": "Nº Máximo de mensajes de transporte no puede ser negativo", + "max-transport-data-points": "Nº Máximo de datapoints transporte (0 - sin límite)", + "max-transport-data-points-required": "Nº Máximo de datapoints transporte requerido.", + "max-transport-data-points-range": "Nº Máximo de datapoints transporte no puede ser negativo", + "max-r-e-executions": "Nº Máximo de ejecuciones de motor de reglas (0 - sin límite)", + "max-r-e-executions-required": "Nº Máximo de ejecuciones de motor de reglas requerido.", + "max-r-e-executions-range": "Nº Máximo de ejecuciones de motor de reglas no puede ser negativo", + "max-j-s-executions": "Nº Máximo de ejecuciones JavaScript (0 - sin límite)", + "max-j-s-executions-required": "Nº Máximo de ejecuciones JavaScript requerido.", + "max-j-s-executions-range": "Nº Máximo de ejecuciones JavaScript no puede ser negativo", + "max-d-p-storage-days": "Nº Máximo de días a grabar en datapoints (0 - sin límite)", + "max-d-p-storage-days-required": "Nº Máximo de días requerido.", + "max-d-p-storage-days-range": "Nº Máximo de días no puede ser negativo", + "default-storage-ttl-days": "Días por defecto grabado TTL (0 - sin límite)", + "default-storage-ttl-days-required": "Días por defecto TTL requerido.", + "default-storage-ttl-days-range": "Días por defecto TTL no puede ser negativo", + "max-rule-node-executions-per-message": "Nº Máximo de ejecuciones (cadena de reglas) por mensaje (0 - sin límite)", + "max-rule-node-executions-per-message-required": "Nº Máximo de ejecuciones por mensaje requerido.", + "max-rule-node-executions-per-message-range": "Nº Máximo de ejecuciones por mensaje no puede ser negativo", + "max-emails": "Nº Máximo de emails (0 - sin límite)", + "max-emails-required": "Nº Máximo de emails requerido.", + "max-emails-range": "Nº Máximo de emails no puede ser negativo", + "max-sms": "Nº Máximo de mensajes SMS (0 - sin límite)", + "max-sms-required": "Nº Máximo de mensajes SMS requerido.", + "max-sms-range": "Nº Máximo de mensajes SMS no puede ser negativo" + }, "timeinterval": { - "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segundos} }", - "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }", - "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }", - "days-interval": "{ days, plural, 1 {1 día} other {# días} }", - "days": "Días", - "hours": "Horas", - "minutes": "Minutos", + "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segundos} }", + "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }", + "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }", + "days-interval": "{ days, plural, 1 {1 día} other {# días} }", + "days": "Días", + "hours": "Horas", + "minutes": "Minutos", + "seconds": "Segundos", + "advanced": "Avanzado" + }, + "timeunit": { "seconds": "Segundos", - "advanced": "Avanzado" + "minutes": "Minutos", + "hours": "Horas", + "days": "Días" }, "timewindow": { "days": "{ days, plural, 1 { día } other {# días } }", @@ -1569,9 +2134,9 @@ "add-user-text": "Agregar nuevo usuario", "no-users-text": "Ningún usuario encontrado", "user-details": "Detalles del usuario", - "delete-user-title": "¿Estás seguro que quieres eliminar el usuario '{{userEmail}}'?", + "delete-user-title": "¿Eliminar el usuario '{{userEmail}}'?", "delete-user-text": "Atención, tras la confirmación el usuario seleccionado será eliminado y la información relacionada será irrecuperable.", - "delete-users-title": "¿Estás seguro que quieres eliminar { count, plural, 1 {1 usuario} other {# usuarios} }?", + "delete-users-title": "¿Eliminar { count, plural, 1 {1 usuario} other {# usuarios} }?", "delete-users-action-title": "Borrar { count, plural, 1 {1 usuario} other {# usuarios} }", "delete-users-text": "Atención, tras la confirmación los usuarios seleccionados serán eliminados y la información relacionada será irrecuperable.", "activation-email-sent-message": "Mail de activación enviado con éxito!", @@ -1597,6 +2162,8 @@ "details": "Detalles", "login-as-tenant-admin": "Iniciar sesión como Administrador Propietario", "login-as-customer-user": "Iniciar sesión como Usuario Cliente", + "search": "Buscar usuarios", + "selected-users": "{ count, plural, 1 {1 usuario} other {# usuarios} } seleccionados", "disable-account": "Deshabilitar cuenta de usuario", "enable-account": "Habilitar cuenta de usuario", "enable-account-message": "¡La cuenta de usuario se ha habilitado correctamente!", @@ -1606,18 +2173,23 @@ "type": "Tipo de valor", "string": "Cadena de texto", "string-value": "Valor de cadena de texto", + "string-value-required": "Se requiere valor de cadena de texto", "integer": "Nro entero", "integer-value": "Valor de nro entero", - "invalid-integer-value": "Valor inválido", + "integer-value-required": "Se requiere valor entero", + "invalid-integer-value": "Valor de entero inválido", "double": "Nro decimal", "double-value": "Valor nro decimal", + "double-value-required": "Se requiere valor nro decimal", "boolean": "Booleano", "boolean-value": "Valor booleano", "false": "Falso", "true": "Verdadero", "long": "Nro Largo", "json": "JSON", - "json-value": "Valor JSON" + "json-value": "Valor JSON", + "json-value-invalid": "El valor JSON tiene un formato inválido", + "json-value-required": "Se requiere valor JSON" }, "widget": { "widget-library": "Bibloteca de Widgets", @@ -1629,7 +2201,7 @@ "widget-type-load-error": "El widget no pudo ser cargado debido a estos errores:", "remove": "Eliminar widget", "edit": "Editar widget", - "remove-widget-title": "¿Estás seguro que quieres eliminar el widget '{{widgetTitle}}'?", + "remove-widget-title": "¿Eliminar el widget '{{widgetTitle}}'?", "remove-widget-text": "Atención, tras la confirmación el widget será eliminado y toda la información relacionada será irrecuperable..", "timeseries": "Series de tiempo", "search-data": "Buscar datos", @@ -1653,6 +2225,7 @@ "type": "Tipo", "resources": "Recursos", "resource-url": "URL JavaScript/CSS", + "resource-is-module": "Es módulo", "remove-resource": "Eliminar recurso", "add-resource": "Agregar recurso", "html": "HTML", @@ -1662,7 +2235,7 @@ "datakey-settings-schema": "Esquema de configuración de clave de datos", "javascript": "Javascript", "js": "JS", - "remove-widget-type-title": "¿Estás seguro que quieres eliminar el tipo del widget '{{widgetName}}'?", + "remove-widget-type-title": "¿Eliminar el tipo del widget '{{widgetName}}'?", "remove-widget-type-text": "Atención, tras la confirmación el tipo será eliminado y la información relacionada será irrecuperable.", "remove-widget-type": "Eliminar tipo de widget.", "add-widget-type": "Agregar nuevo tipo de widget", @@ -1670,7 +2243,10 @@ "widget-template-load-failed-error": "Error al cargar la plantilla del widget!", "add": "Agregar Widget", "undo": "Deshacer cambios", - "export": "Exportar widget" + "export": "Exportar widget", + "no-data": "No hay datos para mostrar en widget", + "data-overflow": "El widget muestra {{count}} de {{total}} entidades", + "alarm-data-overflow": "El widget muestra alarmas para {{allowedEntities}} entidades (máximo permitido) de {{totalEntities}} entidades" }, "widget-action": { "header-button": "Botón de encabezado widget", @@ -1683,7 +2259,14 @@ "target-dashboard-state-required": "Se requiere estado de panel de destino", "set-entity-from-widget": "Establecer entidad desde widget", "target-dashboard": "Panel de destino", - "open-right-layout": "Abrir diseño de panel (derecho)(vista móvil)" + "open-right-layout": "Abrir diseño de panel (derecho)(vista móvil)", + "open-in-separate-dialog": "Abrir en un diálogo separado", + "dialog-title": "Título del diálogo", + "dialog-hide-dashboard-toolbar": "Ocultar barra de herramientas en el diálogo", + "dialog-width": "Ancho de diálogo en porcentaje relativo al ancho del viewport", + "dialog-height": "Alto de diálogo en porcentaje relativo al alto del viewport", + "dialog-size-range-error": "El tamaño del diálogo debe ser entre un rango de 1 a 100", + "open-new-browser-tab": "Abrir en una nueva pestaña" }, "widgets-bundle": { "current": "Paquete actual", @@ -1697,9 +2280,9 @@ "empty": "Paquete de widgets vacío.", "details": "Detalles", "widgets-bundle-details": "Detalles del paquete de Widgets", - "delete-widgets-bundle-title": "¿Estás seguro que quieres eliminar el paquete de widgets '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-title": "¿Eliminar el paquete de widgets '{{widgetsBundleTitle}}'?", "delete-widgets-bundle-text": "Atención, tras la confirmación todos los paquetes seleccionados serán eliminados y su información relacionada será irrecuperable.", - "delete-widgets-bundles-title": "¿Estás seguro que deseas eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }?", + "delete-widgets-bundles-title": "¿Eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }?", "delete-widgets-bundles-action-title": "Eliminar { count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} }", "delete-widgets-bundles-text": "Atención, tras la confirmación todos los paquetes seleccionados serán eliminados y la información relacionada será irrecuperable.", "no-widgets-bundles-matching": "Ningún paquete '{{widgetsBundle}}' encontrado.", @@ -1710,7 +2293,10 @@ "export-failed-error": "Imposible exportar paquete de widgets: {{error}}", "create-new-widgets-bundle": "Crear nuevo paquete de widgets", "widgets-bundle-file": "Archivo de paquete de widgets", - "invalid-widgets-bundle-file-error": "Imposible importar paquete de widgets: Estructura de datos inválida." + "invalid-widgets-bundle-file-error": "Imposible importar paquete de widgets: Estructura de datos inválida.", + "search": "Buscar paquete de widgets", + "selected-widgets-bundles": "{ count, plural, 1 {1 paquete de widgets} other {# paquetes de widgets} } seleccionados", + "open-widgets-bundle": "Abrir paquete de widgets" }, "widget-config": { "data": "Datos", @@ -1749,6 +2335,7 @@ "action": "Acción", "add-action": "Añadir acción", "search-actions": "Buscar acciones", + "no-actions-text": "No se encontraron actiones", "action-source": "Origen de acción", "action-source-required": "Origen de acción requerido.", "action-name": "Nombre", @@ -1760,7 +2347,7 @@ "edit-action": "Editar acción", "delete-action": "Borrar acción", "delete-action-title": "Borrar acción de widget", - "delete-action-text": "Estás seguro de borrar la acción de widget con el nombre '{{actionName}}'?", + "delete-action-text": "Eliminar la acción de widget con el nombre '{{actionName}}'?", "display-icon": "Mostrar icono del título", "icon-color": "Color del icono", "icon-size": "Tamaño del icono" @@ -1828,7 +2415,7 @@ "Custom interval": "Intervalo personalizado", "Interval": "Intervalo", "Step size": "Número de pasos", - "Ok": "De acuerdo" + "Ok": "Ok" } }, "input-widgets": { @@ -1846,8 +2433,11 @@ "entity-coordinate-required": "Se requieren ambos campos (latitud y longitud)", "entity-timeseries-required": "Se requiere la serie de tiempo de la entidad", "get-location": "Obtener localización actual", + "invalid-date": "Fecha inválida", "latitude": "Latitud", "longitude": "Longitud", + "min-value-error": "El valor mínimo es {{value}}", + "max-value-error": "El valor máximo es {{value}}", "not-allowed-entity": "La entidad seleccionada no puede tener atributos compartidos", "no-attribute-selected": "No se seleccionó ningún atributo", "no-datakey-selected": "No se seleccionó ninguna clave de datos", @@ -1856,6 +2446,9 @@ "no-image": "Sin imagen", "no-support-geolocation": "Tu navegador no soporta geolocalización", "no-support-web-camera": "No hay cámara web compatible", + "enable-https-use-widget": "Por favor, activa HTTPS para poder usar este widget", + "no-found-your-camera": "No es posible encontrar la cámara", + "no-permission-camera": "Permiso denegado por el usuario / Esta página no tiene permisos para usar la cámara", "no-timeseries-selected": "No hay series de tiempo seleccionadas", "secret-key": "Clave", "secret-key-required": "Clave requerida", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 670aa58fc5..c33f591272 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -1850,9 +1850,7 @@ "name-required": "Необхідно задати ім'я", "configuration": "Конфігурація", "schedule": "Розклад", - "start": "Початок", - "date": "Дата", - "time": "Час", + "start-time": "Початок", "repeat": "Повтор", "repeats": "Повтори", "daily": "Щодня", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 28e38022a9..7c95423f80 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -73,9 +73,9 @@ "enable-proxy": "启用代理", "enable-tls": "启用TLS", "error-verification-url": "域名不应包含符号 “/” 和 “:”。例:thingsboard.io", - "general": "总则", - "general-policy": "General policy", - "general-settings": "常规设置", + "general": "基本设置", + "general-policy": "基本策略", + "general-settings": "基本设置", "mail-from": "邮件来自", "mail-from-required": "邮件发件人必填。", "max-failed-login-attempts": "登录失败之前的最大登录尝试次数", @@ -91,7 +91,7 @@ "minimum-special-characters-range": "特殊字符的最小数目不能为负数", "minimum-uppercase-letters": "最小大写字母数", "minimum-uppercase-letters-range": "大写字母的最小数目不能为负数", - "number-from": "来自的电话号码", + "number-from": "发送方电话号码", "number-from-required": "发送方电话号码必填。", "number-to": "电话号码至", "number-to-required": "电话号码必填。", @@ -182,11 +182,11 @@ "sms-message": "短信", "sms-message-max-length": "短信长度不能超过1600个字符", "sms-message-required": "短消息内容必填。", - "sms-provider": "SMS provider", - "sms-provider-settings": "SMS provider 设置", - "sms-provider-type": "SMS provider 类型", + "sms-provider": "SMS 服务商", + "sms-provider-settings": "SMS 服务商设置", + "sms-provider-type": "SMS 服务商类型", "sms-provider-type-aws-sns": "亚马逊社交网站", - "sms-provider-type-required": "SMS provider 类型必填。", + "sms-provider-type-required": "SMS 服务商类型必填。", "sms-provider-type-twilio": "Twilio", "smtp-host": "SMTP主机", "smtp-host-required": "SMTP主机必填。", @@ -224,15 +224,15 @@ "acknowledge": "应答", "aknowledge-alarm-text": "确定要确认报警吗?", "aknowledge-alarm-title": "确认报警", - "aknowledge-alarms-text": "确定要确认 { count, plural, 1 {1 个警告} other {# 个警告} }?", - "aknowledge-alarms-title": "确认 { count, plural, 1 {1 个警告} other {# 个警告} }", + "aknowledge-alarms-text": "确定要确认 { count, plural, 1 {# 个警告} other {# 个警告} }?", + "aknowledge-alarms-title": "确认 { count, plural, 1 {# 个警告} other {# 个警告} }", "alarm": "警告", "alarm-details": "报警详细信息", - "alarm-filter": "报警过滤器", + "alarm-filter": "报警筛选器", "alarm-required": "警告必填", "alarm-severity-list": "警报严重性列表", "alarm-status": "警告状态", - "alarm-status-filter": "报警状态过滤器", + "alarm-status-filter": "报警状态筛选器", "alarm-status-list": "报警状态列表", "alarm-type-list": "报警类型列表", "alarms": "警告", @@ -242,11 +242,11 @@ "clear": "清除", "clear-alarm-text": "确定要清除警报吗?", "clear-alarm-title": "清除警报", - "clear-alarms-text": "确定要清除 { count, plural, 1 {1 个警告} other {# 个警告} }?", - "clear-alarms-title": "清除 { count, plural, 1 {1 个警告} other {# 个警告} }", + "clear-alarms-text": "确定要清除 { count, plural, 1 {# 个警告} other {# 个警告} }?", + "clear-alarms-title": "清除 { count, plural, 1 {# 个警告} other {# 个警告} }", "clear-time": "清除时间", "created-time": "创建时间", - "details": "细节", + "details": "详情", "display-status": { "ACTIVE_ACK": "Active 已确认", "ACTIVE_UNACK": "Active 未确认", @@ -268,7 +268,7 @@ "originator-type": "Originator 类型", "polling-interval": "警告轮询间隔(秒)", "polling-interval-required": "警告轮询间隔必填。", - "search": "搜索警告", + "search": "查找警告", "search-propagated-alarms": "检索已传递的警报", "search-status": { "ACK": "已确认", @@ -278,7 +278,7 @@ "UNACK": "未确认" }, "select-alarm": "选择警告", - "selected-alarms": "已选择 { count, plural, 1 {1 个警告} other {# 个警告} }", + "selected-alarms": "已选择 { count, plural, 1 {# 个警告} other {# 个警告} }", "severity": "严重程度", "severity-critical": "危险", "severity-indeterminate": "不确定", @@ -297,9 +297,9 @@ "default-state-entity": "默认状态实体", "duplicate-alias": "别名已经存在。", "edit": "编辑别名", - "entity-filter": "实体过滤", - "entity-filter-no-entity-matched": "未找到符合指定过滤条件的实体。", - "filter-type": "过滤类型", + "entity-filter": "实体筛选器", + "entity-filter-no-entity-matched": "未找到符合指定筛选条件的实体。", + "filter-type": "筛选器类型", "filter-type-apiUsageState": "Api使用状态", "filter-type-asset-search-query": "资产搜索查询", "filter-type-asset-search-query-description": "类型为 {{assetTypes}} 且具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的资产", @@ -320,15 +320,15 @@ "filter-type-entity-view-type-description": "类型为 '{{entityView}}' 的实体视图", "filter-type-relations-query": "关联查询", "filter-type-relations-query-description": "具有 {{relationType}} 关联 {{direction}} {{rootEntity}} 的 {{entities}} ", - "filter-type-required": "过滤类型必填。", + "filter-type-required": "筛选器类型必填。", "filter-type-single-entity": "单个实体", - "filter-type-state-entity": "实体(仪表板状态)", - "filter-type-state-entity-description": "实体令牌(仪表板状态参数)", + "filter-type-state-entity": "仪表板实体状态", + "filter-type-state-entity-description": "仪表板实体令牌状态参数", "last-level-relation": "仅获取最后一级关联", "max-relation-level": "最大关联层级", "name": "别名", "name-required": "别名必填", - "no-entity-filter-specified": "没有指定实体过滤条件", + "no-entity-filter-specified": "没有指定实体筛选器", "resolve-multiple": "解决为多实体", "root-entity": "根实体", "root-state-entity": "使用仪表板状态实体作为根实体", @@ -339,7 +339,7 @@ "api-usage": { "api-usage": "Api 使用统计", "data-points": "数据点", - "data-points-storage-days": "数据点存储天数", + "data-points-storage-days": "日存储数据点数", "email": "Email", "email-messages": "Email messages", "email-messages-daily-activity": "Email messages daily activity", @@ -354,7 +354,7 @@ "javascript-functions-hourly-activity": "JavaScript functions hourly activity", "javascript-functions-monthly-activity": "JavaScript functions monthly activity", "latest-error": "最新错误", - "messages": "信息", + "messages": "消息", "permanent-failures": "${entityName} 永久性故障", "permanent-timeouts": "${entityName} 永久超时", "processing-failures": "${entityName} 处理失败", @@ -381,11 +381,11 @@ "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 daily activity", "transport-data-points": "传输数据点", "transport-hourly-activity": "Transport hourly activity", "transport-messages": "传输消息", - "transport-monthly-activity": "运输月度活动", + "transport-monthly-activity": "Transport monthly activity", "view-details": "查看详细信息", "view-statistics": "查看统计信息" }, @@ -406,7 +406,7 @@ "assign-asset-to-customer": "将资产分配给客户", "assign-asset-to-customer-text": "请选择要分配给客户的资产", "assign-assets": "分配资产", - "assign-assets-text": "分配 { count, plural, 1 {1 个资产} other {# 个资产} } 给客户", + "assign-assets-text": "分配 { count, plural, 1 {# 个资产} other {# 个资产} } 给客户", "assign-new-asset": "分配新资产", "assign-to-customer": "分配给客户", "assign-to-customer-text": "请选择客户以分配资产", @@ -416,11 +416,11 @@ "delete-asset-text": "小心!确认后资产及其所有相关数据将不可恢复。", "delete-asset-title": "确定要删除资产 '{{assetName}}'?", "delete-assets": "删除资产", - "delete-assets-action-title": "删除 { count, plural, 1 {1 个资产} other {# 个资产} }", + "delete-assets-action-title": "删除 { count, plural, 1 {# 个资产} other {# 个资产} }", "delete-assets-text": "请注意:确认后,所有选定的资产将被删除,所有相关的数据将变得不可恢复。", - "delete-assets-title": "确定要删除 { count, plural, 1 {1 个资产} other {# 个资产} }?", + "delete-assets-title": "确定要删除 { count, plural, 1 {# 个资产} other {# 个资产} }?", "description": "说明", - "details": "细节", + "details": "详情", "enter-asset-type": "输入资产类型", "events": "事件", "idCopiedMessage": "资产ID已经复制到粘贴板", @@ -440,19 +440,19 @@ "no-assets-matching": "没有找到匹配 '{{entity}}' 的资产。", "no-assets-text": "未找到资产", "public": "公开", - "search": "搜索资产", + "search": "查找资产", "select-asset": "选择资产", "select-asset-type": "选择资产类型", - "selected-assets": "已选择 { count, plural, 1 {1 个资产} other {# 个资产} }", + "selected-assets": "已选择 { count, plural, 1 {# 个资产} other {# 个资产} }", "type": "类型", "type-required": "类型必填。", "unassign-asset": "未分配资产", "unassign-asset-text": "确认后,资产将未分配,客户无法访问。", "unassign-asset-title": "您确定要取消对'{{assetName}}'资产的分配吗?", "unassign-assets": "取消分配资产", - "unassign-assets-action-title": "从客户处取消分配 { count, plural, 1 {1 个资产} other {# 个资产} }", + "unassign-assets-action-title": "从客户处取消分配 { count, plural, 1 {# 个资产} other {# 个资产} }", "unassign-assets-text": "确认后,所有选定的资产将被分配,客户无法访问。", - "unassign-assets-title": "您确定要取消分配 { count, plural, 1 {1 个资产} other {# 个资产} }吗?", + "unassign-assets-title": "您确定要取消分配 { count, plural, 1 {# 个资产} other {# 个资产} }吗?", "unassign-from-customer": "取消分配客户", "view-assets": "查看资产" }, @@ -464,7 +464,7 @@ "attributes-scope": "设备属性范围", "delete-attributes": "删除属性", "delete-attributes-text": "注意,确认后所有选中的属性都会被删除。", - "delete-attributes-title": "您确定要删除 { count, plural, 1 {1 个属性} other {# 个属性} }吗?", + "delete-attributes-title": "您确定要删除 { count, plural, 1 {# 个属性} other {# 个属性} }吗?", "enter-attribute-value": "输入属性值", "key": "键名", "key-required": "属性键必填。", @@ -478,8 +478,8 @@ "scope-latest-telemetry": "最新遥测数据", "scope-server": "服务端属性", "scope-shared": "共享属性", - "selected-attributes": "已选择{ count, plural, 1 {1 个属性} other {# 个属性} }", - "selected-telemetry": "已选择 { count, plural, 1 {1 telemetry unit} other {# telemetry units} }", + "selected-attributes": "已选择{ count, plural, 1 {# 个属性} other {# 个属性} }", + "selected-telemetry": "已选择 { count, plural, 1 {# telemetry unit} other {# telemetry units} }", "show-on-widget": "在部件上显示", "value": "价值", "value-required": "属性值必填。", @@ -491,7 +491,7 @@ "audit-log-details": "审计日志详情", "audit-logs": "审计日志", "clear-search": "清空查找", - "details": "细节", + "details": "详情", "entity-name": "实体名称", "entity-type": "实体类型", "failure-details": "失败详情", @@ -591,11 +591,11 @@ "delete": "删除此客户", "delete-customer-text": "小心!确认后,客户及其所有相关数据将不可恢复。", "delete-customer-title": "您确定要删除客户'{{customerTitle}}'吗?", - "delete-customers-action-title": "删除 { count, plural, 1 {1 个客户} other {# 个客户} }", + "delete-customers-action-title": "删除 { count, plural, 1 {# 个客户} other {# 个客户} }", "delete-customers-text": "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。", - "delete-customers-title": "您确定要删除 { count, plural, 1 {1 个客户} other {# 个客户} }吗?", + "delete-customers-title": "您确定要删除 { count, plural, 1 {# 个客户} other {# 个客户} }吗?", "description": "说明", - "details": "细节", + "details": "详情", "devices": "客户设备", "entity-views": "客户实体视图", "events": "事件", @@ -618,10 +618,10 @@ "public-dashboards": "公共仪表板", "public-devices": "公共设备", "public-entity-views": "公共实体视图", - "search": "搜索客户", + "search": "查找客户", "select-customer": "选择客户", "select-default-customer": "选择默认的客户", - "selected-customers": "已选择 { count, plural, 1 {1 个客户} other {# 个客户} }", + "selected-customers": "已选择 { count, plural, 1 {# 个客户} other {# 个客户} }", "title": "标题", "title-required": "标题必填。" }, @@ -634,7 +634,7 @@ "assign-dashboard-to-customer": "将仪表板分配给客户", "assign-dashboard-to-customer-text": "请选择要分配给客户的仪表板", "assign-dashboards": "分配仪表板", - "assign-dashboards-text": "分配 { count, plural, 1 {1 个仪表板} other {# 个仪表板} } 给客户", + "assign-dashboards-text": "分配 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 给客户", "assign-new-dashboard": "分配新的仪表板", "assign-to-customer": "分配给客户", "assign-to-customer-text": "请选择客户分配仪表板", @@ -665,19 +665,19 @@ "delete-dashboard-text": "小心!确认后仪表板及其所有相关数据将不可恢复。", "delete-dashboard-title": "您确定要删除仪表板 '{{dashboardTitle}}'吗?", "delete-dashboards": "删除仪表板", - "delete-dashboards-action-title": "删除 { count, plural, 1 {1 个仪表板} other {# 个仪表板} }", + "delete-dashboards-action-title": "删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }", "delete-dashboards-text": "小心!确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。", - "delete-dashboards-title": "确定要删除 { count, plural, 1 {1 个仪表板} other {# 个仪表板} }吗?", + "delete-dashboards-title": "确定要删除 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }吗?", "delete-state": "删除仪表板状态", "delete-state-text": "确定要删除仪表板状态 '{{stateName}}' 吗?", "delete-state-title": "删除仪表板状态", "description": "说明", - "details": "细节", + "details": "详情", "display-dashboard-export": "显示导出", "display-dashboard-timewindow": "显示时间窗口", "display-dashboards-selection": "显示仪表板选项", "display-entities-selection": "显示实体选项", - "display-filters": "显示过滤器", + "display-filters": "显示筛选器", "display-title": "显示仪表板标题", "drop-image": "拖拽图像或单击以选择要上传的文件。", "edit-state": "仪表板状态编辑", @@ -688,7 +688,7 @@ "horizontal-margin-required": "需要水平边距值。", "import": "导入仪表板", "import-widget": "导入部件", - "invalid-aliases-config": "无法找到与某些别名过滤器匹配的任何设备。
请联系您的管理员以解决此问题。", + "invalid-aliases-config": "无法找到与某些别名筛选器匹配的任何设备。
请联系您的管理员以解决此问题。", "invalid-dashboard-file-error": "无法导入仪表板: 仪表板数据结构无效。", "invalid-widget-file-error": "无法导入窗口部件: 窗口部件数据结构无效。", "is-root-state": "根状态", @@ -728,7 +728,7 @@ "public-dashboard-title": "仪表板现已公开", "public-link": "公共链接", "public-link-copied-message": "仪表板的公共链接已被复制到剪贴板", - "search": "搜索仪表板", + "search": "查找仪表板", "search-states": "仪表板状态检索", "select-dashboard": "选择仪表板", "select-devices": "选择设备", @@ -736,8 +736,8 @@ "select-state": "选择目标状态", "select-widget-subtitle": "可用的部件类型列表", "select-widget-title": "选择部件", - "selected-dashboards": "已选择 { count, plural, 1 {1 个仪表盘} other {# 个仪表盘} }", - "selected-states": "已选择 { count, plural, 1 {1 个仪表板状态} other {# 个仪表板状态} }", + "selected-dashboards": "已选择 { count, plural, 1 {# 个仪表盘} other {# 个仪表盘} }", + "selected-states": "已选择 { count, plural, 1 {# 个仪表板状态} other {# 个仪表板状态} }", "set-background": "设置背景", "settings": "设置", "show-details": "显示详情", @@ -759,10 +759,10 @@ "unassign-dashboard-text": "确认后,面板将被取消分配,客户将无法访问。", "unassign-dashboard-title": "您确定要取消分配仪表板 '{{dashboardTitle}}'吗?", "unassign-dashboards": "取消分配仪表板", - "unassign-dashboards-action-text": "取消分配 { count, plural, 1 {1 个仪表板} other {# 个仪表板} } 给客户", - "unassign-dashboards-action-title": "取消分配此客户 { count, plural, 1 {1 个仪表板} other {# 个仪表板} }", + "unassign-dashboards-action-text": "取消分配 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 给客户", + "unassign-dashboards-action-title": "取消分配此客户 { count, plural, 1 {# 个仪表板} other {# 个仪表板} }", "unassign-dashboards-text": "确认后,所有选定的仪表板将被取消分配,客户将无法访问。", - "unassign-dashboards-title": "确定要取消分配仪表板 { count, plural, 1 {1 个仪表板} other {# 个仪表板} } 吗?", + "unassign-dashboards-title": "确定要取消分配仪表板 { count, plural, 1 {# 个仪表板} other {# 个仪表板} } 吗?", "unassign-from-customer": "取消分配客户", "unassign-from-customers": "客户未分配仪表板", "unassign-from-customers-text": "请选择从仪表板中取消分配的客户", @@ -787,8 +787,8 @@ "function-types": "函数类型", "function-types-required": "需要函数类型。", "label": "标签", - "maximum-function-types": "最多允许 { count, plural, 1 {1 个函数类型} other {# 个函数类型} }", - "maximum-timeseries-or-attributes": "最多允许 { count, plural, 1 {1 个 timeseries/属性。} other {# 个 timeseries/属性。} }", + "maximum-function-types": "最多允许 { count, plural, 1 {# 个函数类型} other {# 个函数类型} }", + "maximum-timeseries-or-attributes": "最多允许 { count, plural, 1 {# 个 timeseries/属性。} other {# 个 timeseries/属性。} }", "prev-orig-value-description": "先前的原始值;", "prev-value-description": "上一次函数调用的结果;", "settings": "设置", @@ -813,7 +813,7 @@ "time-to": "时间到" }, "details": { - "details": "细节", + "details": "详情", "edit-json": "编辑JSON", "edit-mode": "编辑模式", "toggle-edit-mode": "切换编辑模式" @@ -821,14 +821,14 @@ "device-profile": { "add": "添加设备配置", "add-alarm-rule": "添加报警规则", - "add-alarm-rule-details": "添加详细信息", + "add-alarm-rule-details": "详情模板:", "add-clear-alarm-rule": "添加清除条件", "add-create-alarm-rule": "添加创建条件", "add-create-alarm-rule-prompt": "请添加创建报警规则", "advanced-settings": "高级设置", "alarm-details": "报警详细信息", "alarm-rule-condition": "报警规则条件", - "alarm-rule-details": "细节", + "alarm-rule-details": "详情", "alarm-rule-relation-types-list": "要传递的关联类型", "alarm-rule-relation-types-list-hint": "如果未选择传递关联类型,则将不按关联类型过滤而传递报警。", "alarm-rules": "报警规则", @@ -852,7 +852,7 @@ "condition-duration-value-range": "持续时间值应在1到2147483647之间。", "condition-duration-value-required": "持续时间值必填。", "condition-during": "在 {{during}} 期间", - "condition-repeat-times": "重复 { count, plural, 1 {1 次} other {# 次} }", + "condition-repeat-times": "重复 { count, plural, 1 {# 次} other {# 次} }", "condition-repeating-value": "事件计数", "condition-repeating-value-pattern": "事件计数应为整数。", "condition-repeating-value-range": "事件计数应在1到2147483647之间。", @@ -874,7 +874,7 @@ "delete-device-profile-text": "注意,确认后设备配置和所有相关数据将不可恢复。", "delete-device-profile-title": "是否确实要删除设备配置 '{{deviceProfileName}}'?", "delete-device-profiles-text": "请注意:确认后,所有选定的设备配置将被删除,所有相关数据将不可恢复。", - "delete-device-profiles-title": "确定要删除 { count, plural, 1 {1 个设备配置} other {# 个设备配置} }?", + "delete-device-profiles-title": "确定要删除 { count, plural, 1 {# 个设备配置} other {# 个设备配置} }?", "description": "说明", "device-profile": "设备配置", "device-profile-details": "设备配置详情", @@ -886,13 +886,13 @@ "edit-alarm-rule-condition": "编辑报警规则条件", "edit-schedule": "编辑报警日程表", "enter-alarm-rule-condition-prompt": "请添加报警规则条件", - "idCopiedMessage": "设备配置 Id 已复制到剪贴板", + "idCopiedMessage": "设备配置 ID 已复制到剪贴板", "mqtt-device-payload-type": "MQTT 设备 Payload", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", "mqtt-device-topic-filters": "MQTT 设备 Topic 筛选器", "mqtt-device-topic-filters-unique": "MQTT设备 Topic 筛选器必须唯一。", - "mqtt-payload-type-required": "荷载类型必填。", + "mqtt-payload-type-required": "Payload 类型必填。", "multi-level-wildcards-hint": "[#]可以替换 topic filter 本身,并且必须是 topic 的最后一个符号。例如:# or v1/devices/me/#。", "name": "名称", "name-required": "名称是必需的。", @@ -922,9 +922,9 @@ "provision-strategy-required": "预配置策略必填。", "rpc-response-topic-filter": "RPC响应 Topic 筛选器", "rpc-response-topic-filter-required": "RPC响应 Topic 筛选器必填。", - "schedule": "地铁列车时刻表", - "schedule-any-time": "Active all the time", - "schedule-custom": "定制", + "schedule": "启用规则:", + "schedule-any-time": "始终启用", + "schedule-custom": "自定义启用", "schedule-day": { "friday": "星期五", "monday": "星期一", @@ -936,16 +936,16 @@ }, "schedule-days": "天", "schedule-days-of-week-required": "每周至少选择一天。", - "schedule-specific-time": "在特定时间活跃", + "schedule-specific-time": "定时启用", "schedule-time": "时间", "schedule-time-from": "从", "schedule-time-to": "到", "schedule-type": "计划程序类型", "schedule-type-required": "计划类型必填。", - "search": "搜索设备配置", + "search": "查找设备配置", "select-alarm-severity": "选择报警严重性", "select-queue-hint": "从下拉列表中选择或添加自定义名称。", - "selected-device-profiles": "已选择 { count, plural, 1 {1 个设备配置} other {# 个设备配置} }", + "selected-device-profiles": "已选择 { count, plural, 1 {# 个设备配置} other {# 个设备配置} }", "set-default": "设为默认设备配置", "set-default-device-profile-text": "确认后,设备配置将被标记为默认,并将用于未指定配置的新设备。", "set-default-device-profile-title": "确实要将设备配置 '{{deviceProfileName}}' 设为默认值吗?", @@ -983,7 +983,7 @@ "assign-device-to-customer": "将设备分配给客户", "assign-device-to-customer-text": "请选择要分配给客户的设备", "assign-devices": "分配设备", - "assign-devices-text": "将 {count,plural,1 {1 个设备} other {# 个设备} }分配给客户", + "assign-devices-text": "将 {count,plural,1 {# 个设备} other {# 个设备} }分配给客户", "assign-new-device": "分配新设备", "assign-to-customer": "分配给客户", "assign-to-customer-text": "请选择客户分配设备", @@ -1003,11 +1003,11 @@ "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。", "delete-device-title": "您确定要删除设备的{{deviceName}}吗?", "delete-devices": "删除设备", - "delete-devices-action-title": "删除 {count,plural,1 {1 个设备} other {# 个设备} }", + "delete-devices-action-title": "删除 {count,plural,1 {# 个设备} other {# 个设备} }", "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。", - "delete-devices-title": "确定要删除{count,plural,1 {1 个设备} other {# 个设备} } 吗?", + "delete-devices-title": "确定要删除{count,plural,1 {# 个设备} other {# 个设备} } 吗?", "description": "说明", - "details": "细节", + "details": "详情", "device": "设备", "device-alias": "设备别名", "device-configuration": "设备配置", @@ -1017,7 +1017,7 @@ "device-list": "设备列表", "device-list-empty": "没有被选中的设备", "device-name-filter-no-device-matched": "找不到以'{{device}}' 开头的设备。", - "device-name-filter-required": "设备名称过滤器必填。", + "device-name-filter-required": "设备名称筛选器必填。", "device-public": "设备公开", "device-required": "设备必填", "device-type": "设备类型", @@ -1056,12 +1056,12 @@ "remove-alias": "删除设备别名", "rsa-key": "RSA公钥", "rsa-key-required": "RSA公钥必填", - "search": "搜索设备", + "search": "查找设备", "secret": "密钥", "secret-required": "密钥必填", "select-device": "选择设备", "select-device-type": "选择设备类型", - "selected-devices": "已选择 { count, plural, 1 {1 个设备} other {# 个设备} }", + "selected-devices": "已选择 { count, plural, 1 {# 个设备} other {# 个设备} }", "transport-configuration": "传输配置", "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用:
{{widgetsList}}", "unable-delete-device-alias-title": "无法删除设备别名", @@ -1069,11 +1069,11 @@ "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。", "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?", "unassign-devices": "取消分配设备", - "unassign-devices-action-title": "取消分配此客户 {count,plural,1 {1 个设备} other {# 个设备} }", + "unassign-devices-action-title": "取消分配此客户 {count,plural,1 {# 个设备} other {# 个设备} }", "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。", - "unassign-devices-title": "确定要取消分配 {count,plural,1 {1 个设备} other {# 个设备} } 吗?", + "unassign-devices-title": "确定要取消分配 {count,plural,1 {# 个设备} other {# 个设备} } 吗?", "unassign-from-customer": "取消分配客户", - "use-device-name-filter": "使用过滤器", + "use-device-name-filter": "使用筛选器", "user-name": "用户名", "user-name-required": "用户名必填。", "view-credentials": "查看凭据", @@ -1123,7 +1123,7 @@ "assign-entity-view-to-customer": "将实体视图分配给客户", "assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图", "assign-entity-views": "分配实体视图", - "assign-entity-views-text": "分配 { count, plural, 1 {1 个实体视图} other {# 个实体视图} } 给客户", + "assign-entity-views-text": "分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} } 给客户", "assign-new-entity-view": "分配新实体视图", "assign-to-customer": "分配给客户", "assign-to-customer-text": "请选择客户分配实体视图", @@ -1142,11 +1142,11 @@ "delete-entity-view-text": "小心!确认后实体视图及其所有相关数据将不可恢复。", "delete-entity-view-title": "确定要删除实体视图 '{{entityViewName}}'?", "delete-entity-views": "删除实体视图", - "delete-entity-views-action-title": "删除 { count, plural, 1 {1 个实体视图} other {# 个实体视图} }", + "delete-entity-views-action-title": "删除 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }", "delete-entity-views-text": "请注意:确认后,所有选定的实体视图将被删除,所有相关的数据将变得不可恢复。", - "delete-entity-views-title": "确定要删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }?", + "delete-entity-views-title": "确定要删除 { count, plural, 1 {# 实体视图} other {# 实体视图} }?", "description": "说明", - "details": "细节", + "details": "详情", "duplicate-alias-error": "找到重复别名 '{{alias}}'。
实体视图别名必须是唯一的。", "end-date": "结束日期", "end-ts": "结束时间", @@ -1157,7 +1157,7 @@ "entity-view-list": "实体视图列表", "entity-view-list-empty": "没有被选中的实体视图", "entity-view-name-filter-no-entity-view-matched": "找不到以'{{entityView}}' 开头的实体视图。", - "entity-view-name-filter-required": "实体视图名称过滤器必填。", + "entity-view-name-filter-required": "实体视图名称筛选器必填。", "entity-view-public": "实体视图是公共的", "entity-view-required": "实体视图必填。", "entity-view-type": "实体视图类型", @@ -1186,10 +1186,10 @@ "no-keys-found": "找不到密钥。", "public": "公开", "remove-alias": "删除实体视图别名", - "search": "搜索实体视图", + "search": "查找实体视图", "select-entity-view": "选择实体视图", "select-entity-view-type": "选择实体视图类型", - "selected-entity-views": "已选择 { count, plural, 1 {1 个实体视图} other {# 个实体视图} }", + "selected-entity-views": "已选择 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }", "server-attributes": "服务端属性", "server-attributes-placeholder": "服务端属性", "shared-attributes": "共享属性", @@ -1207,11 +1207,11 @@ "unassign-entity-view-text": "确认后,实体视图将未分配,客户无法访问。", "unassign-entity-view-title": "您确定要取消对 '{{entityViewName}}'实体视图的分配吗?", "unassign-entity-views": "取消分配实体视图", - "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {1 实体视图} other {# 实体视图} }", + "unassign-entity-views-action-title": "从客户处取消分配{count,plural,1 {# 实体视图} other {# 实体视图} }", "unassign-entity-views-text": "确认后,所有选定的实体视图将被分配,客户无法访问。", - "unassign-entity-views-title": "确定要取消分配 { count, plural, 1 {1 个实体视图} other {# 个实体视图} }吗?", + "unassign-entity-views-title": "确定要取消分配 { count, plural, 1 {# 个实体视图} other {# 个实体视图} }吗?", "unassign-from-customer": "取消分配客户", - "use-entity-view-name-filter": "使用过滤器", + "use-entity-view-name-filter": "使用筛选器", "view-entity-views": "查看实体视图" }, "entity": { @@ -1242,14 +1242,14 @@ "entity-list-empty": "没有选择实体。", "entity-name": "实体名", "entity-name-filter-no-entity-matched": "没有找到以 '{{entity}}' 开头的实体", - "entity-name-filter-required": "实体名过滤器必填。", + "entity-name-filter-required": "实体名筛选器必填。", "entity-type": "实体类型", "entity-type-list": "实体类型列表", "entity-type-list-empty": "没有选择实体类型。", "entity-types": "实体类型", "entity-view-name-starts-with": "以 '{{prefix}}' 开头的实体视图", "key": "键名", - "key-name": "密钥名称", + "key-name": "Key name", "list-of-alarms": "{ count, plural, 1 {个警告} other {# 个警告} }", "list-of-assets": "{ count, plural, 1 {个资产} other {# 个资产} }", "list-of-customers": "{ count, plural, 1 {个客户} other {# 个客户} }", @@ -1281,7 +1281,7 @@ "rulenode-name-starts-with": "名称以 '{{prefix}}' 开头的规则节点", "search": "实体检索", "select-entities": "选择实体", - "selected-entities": "已选择 { count, plural, 1 {1 个实体} other {# 个实体} }", + "selected-entities": "已选择 { count, plural, 1 {# 个实体} other {# 个实体} }", "tenant-name-starts-with": "以 '{{prefix}}' 开头的租户", "tenant-profile-name-starts-with": "名称以 '{{prefix}}' 开头的租户配置", "type": "类型", @@ -1321,7 +1321,7 @@ "type-users": "用户", "unable-delete-entity-alias-text": "实体别名 '{{entityAlias}}' 被以下部件使用不能删除:
{{widgetsList}}", "unable-delete-entity-alias-title": "无法删除实体别名", - "use-entity-name-filter": "用户过滤", + "use-entity-name-filter": "用户筛选器", "user-name-starts-with": "以 '{{prefix}}' 开头的用户" }, "error": { @@ -1374,9 +1374,9 @@ "add-timeseries": "添加 Timeseries", "anonymous": "匿名", "attr-json-key-expression": "属性键JSON表达式", - "attr-topic-key-expression": "属性关键字主题表达式", + "attr-topic-key-expression": "属性键名Topic表达式", "attribute-filter": "属性筛选器", - "attribute-key-expression": "属性关键字表达式", + "attribute-key-expression": "属性键名表达式", "attribute-requests": "属性请求", "attribute-updates": "属性更新", "attributes": "属性", @@ -1400,7 +1400,7 @@ "delete-extension-text": "请注意:确认后,扩展和所有相关数据将变得不可恢复。", "delete-extension-title": "确实要删除扩展名'{{extensionId}}'吗?", "delete-extensions-text": "请注意:确认后,所有选定的扩展将被删除。", - "delete-extensions-title": "确定要删除 { count, plural, 1 {1 个扩展} other {# 个扩展} }吗?", + "delete-extensions-title": "确定要删除 { count, plural, 1 {# 个扩展} other {# 个扩展} }吗?", "device-name-expression": "设备名称表达式", "device-name-filter": "设备名称筛选器", "device-type-expression": "设备类型表达式", @@ -1414,7 +1414,7 @@ "extensions": "扩展", "field-required": "必填字段", "file": "扩展文件", - "filter-expression": "过滤表达式", + "filter-expression": "筛选条件表达式", "host": "主机", "id": "ID", "import-extension": "导入扩展", @@ -1488,7 +1488,7 @@ "response-timeout": "毫秒内响应超时", "response-topic-expression": "响应主题表达式", "retry-interval": "以毫秒为单位的重试间隔", - "selected-extensions": "已选择 { count, plural, 1 {1 个扩展} other {# 个扩展} }", + "selected-extensions": "已选择 { count, plural, 1 {# 个扩展} other {# 个扩展} }", "server-side-rpc": "服务端RPC", "ssl": "Ssl", "sync": { @@ -1504,7 +1504,7 @@ "token": "安全令牌", "topic": "主题", "topic-expression": "主题表达", - "topic-filter": "主题滤波", + "topic-filter": "Topic筛选器", "topic-name-expression": "设备名称主题表达式", "topic-type-expression": "设备类型主题表达式", "transformer": "转换器", @@ -1517,14 +1517,14 @@ }, "filter": { "add": "添加筛选器", - "add-complex": "Add complex", + "add-complex": "添加复合", "add-complex-filter": "添加复合筛选器", "add-filter": "添加筛选器", "add-filter-prompt": "请添加筛选器", - "add-key-filter": "添加密钥筛选器", + "add-key-filter": "添加键名筛选器", "autogenerated-label": "自动生成标签", "complex-filter": "复合筛选器", - "create-new-filter": "创建一个新的!", + "create-new-filter": "请新增!", "current-customer": "当前客户", "current-device": "当前设备", "current-tenant": "当前租户", @@ -1538,26 +1538,26 @@ "edit": "编辑筛选器", "edit-complex-filter": "编辑复合筛选器", "edit-filter-user-params": "编辑筛选器谓词用户参数", - "edit-key-filter": "Edit key filter", + "edit-key-filter": "编辑键名筛选器", "editable": "可编辑", "filter": "筛选器", "filter-required": "筛选器必填。", "filter-user-params": "过滤谓词用户参数", - "filters": "过滤器", + "filters": "筛选器", "ignore-case": "忽略大小写", - "key-filter": "Key filter", - "key-filters": "Key filters", - "key-name": "Key name", - "key-name-required": "Key name 必填。", + "key-filter": "键名筛选器", + "key-filters": "键名筛选器", + "key-name": "键名", + "key-name-required": "键名必填。", "key-type": { "attribute": "属性", - "entity-field": "实体字段", - "key-type": "Key type", + "entity-field": "实体", + "key-type": "键类型", "timeseries": "Timeseries" }, "key-value-type-change-message": "如果您确认新的值类型,所有输入的键过滤器将被删除。", "key-value-type-change-title": "是否确实要更改键值类型?", - "missing-key-filters-error": "Key filters is missing for filter '{{filter}}'.", + "missing-key-filters-error": "筛选器 '{{filter}}' 的键名筛选条件缺失。", "name": "筛选器名称", "name-required": "筛选器名称必填。", "no-dynamic-value": "无动态值", @@ -1565,18 +1565,18 @@ "no-filter-text": "未指定筛选器", "no-filters": "未配置筛选器", "no-filters-found": "找不到筛选器。", - "no-key-filters": "未配置密钥筛选器", + "no-key-filters": "未配置键名筛选器", "operation": { "and": "和", "contains": "包含", "ends-with": "结束于", - "equal": "平等的", + "equal": "等于", "greater": "大于", "greater-or-equal": "大于或等于", "less": "小于", "less-or-equal": "小于或等于", "not-contains": "不包含", - "not-equal": "不相等", + "not-equal": "不等于", "operation": "操作", "or": "或", "starts-with": "开始于" @@ -1584,7 +1584,7 @@ "order-priority": "字段顺序优先级", "preview": "筛选器预览", "remove-filter": "删除筛选器", - "remove-key-filter": "Remove key filter", + "remove-key-filter": "删除键名筛选器", "source-attribute": "源属性", "switch-to-default-value": "切换到默认值", "switch-to-dynamic-value": "切换到动态值", @@ -1592,7 +1592,7 @@ "unable-delete-filter-text": "无法删除筛选器 '{{filter}}' ,因为它由以下小部件使用:
{{widgetsList}}", "unable-delete-filter-title": "无法删除筛选器", "user-parameters": "用户参数", - "value": "价值", + "value": "值", "value-type-required": "键值类型是必需的。", "value-type": { "boolean": "布尔值", @@ -1646,7 +1646,7 @@ "access-token": "访问令牌", "tls": "TLS" }, - "storage": "Storage", + "storage": "存储", "storage-max-file-records": "文件中的最大记录数", "storage-max-files": "最大文件数", "storage-max-files-min": "最小值为1。", @@ -1665,7 +1665,7 @@ "storage-type": "存储类型", "storage-types": { "file-storage": "文件存储", - "memory-storage": "存储器" + "memory-storage": "内存存储" }, "thingsboard": "ThingsBoard", "thingsboard-host": "ThingsBoard主机", @@ -1691,9 +1691,9 @@ "delete-item-text": "注意,确认后此项及其所有相关数据将变得不可恢复。", "delete-item-title": "您确定要删除此项吗?", "delete-items": "删除项目", - "delete-items-action-title": "删除 { count, plural, 1 {1 个元素} other {# 个元素} }", + "delete-items-action-title": "删除 { count, plural, 1 {# 个元素} other {# 个元素} }", "delete-items-text": "注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。", - "delete-items-title": "确定你要删除 { count, plural, 1 {1 项} other {# 项} }吗?", + "delete-items-title": "确定你要删除 { count, plural, 1 {# 项} other {# 项} }吗?", "item-details": "项目详细信息", "no-items-text": "没有找到项目", "scroll-to-top": "滚动到顶部" @@ -1865,7 +1865,7 @@ }, "relation": { "add": "添加关联", - "add-relation-filter": "添加关联过滤器", + "add-relation-filter": "添加关联筛选器", "additional-info": "附加信息 (JSON)", "any-relation": "任意关联", "any-relation-type": "任何类型", @@ -1873,11 +1873,11 @@ "delete-from-relation-text": "确定删除后,当前实体将与实体 '{{entityName}}' 取消关联", "delete-from-relation-title": "确定要从实体 '{{entityName}}' 删除关联吗?", "delete-from-relations-text": "确定删除所有选择的关联关系后,当前实体将与对应的实体取消关联", - "delete-from-relations-title": "确定删除 { count, plural, 1 {1 个关联} other {# 个关联} } 吗?", + "delete-from-relations-title": "确定删除 { count, plural, 1 {# 个关联} other {# 个关联} } 吗?", "delete-to-relation-text": "确定删除后实体 '{{entityName}}' 将取消与当前实体的关联关系。", "delete-to-relation-title": "确定要删除实体 '{{entityName}}' 的关联吗?", "delete-to-relations-text": "确定删除所有选择的关联关系后,与当前实体对应的所有关联关系将被移除。", - "delete-to-relations-title": "确定要删除 { count, plural, 1 {1 个关联} other {# 个关联} }?", + "delete-to-relations-title": "确定要删除 { count, plural, 1 {# 个关联} other {# 个关联} }?", "direction": "方向", "direction-type": { "FROM": "从", @@ -1890,16 +1890,16 @@ "from-relations": "向外的关联", "invalid-additional-info": "无法解析附加信息JSON。", "no-relations-text": "未找到关联", - "relation-filters": "关联过滤器", + "relation-filters": "关联筛选器", "relation-type": "关联类型", "relation-type-required": "关联类型必填", "relations": "关联", - "remove-relation-filter": "移除关联过滤器", + "remove-relation-filter": "移除关联筛选器", "search-direction": { "FROM": "从", "TO": "到" }, - "selected-relations": "已选择{ count, plural, 1 {1 个关联} other {# 个关联} }", + "selected-relations": "已选择{ count, plural, 1 {# 个关联} other {# 个关联} }", "to-entity": "到实体", "to-entity-name": "到实体名称", "to-entity-type": "到实体类型", @@ -1915,11 +1915,11 @@ "delete": "删除规则链", "delete-rulechain-text": "小心,在确认规则链和所有相关数据将变得不可恢复。", "delete-rulechain-title": " 确实要删除规则链'{{ruleChainName}}'吗?", - "delete-rulechains-action-title": "删除 { count, plural, 1 {1 个规则链} other {# 个规则链} }", + "delete-rulechains-action-title": "删除 { count, plural, 1 {# 个规则链} other {# 个规则链} }", "delete-rulechains-text": "请注意:确认后,所有选定的规则链将被删除,所有相关的数据将变得不可恢复。", "delete-rulechains-title": "确实要删除{count, plural, 1 { 1 个规则链} other {# 个规则链} }吗?", "description": "说明", - "details": "细节", + "details": "详情", "events": "事件", "export": "导出规则链", "export-failed-error": "无法导出规则链:{{error}}", @@ -1938,9 +1938,9 @@ "rulechain-file": "规则链文件", "rulechain-required": "规则链必填", "rulechains": "规则链库", - "search": "搜索规则链", + "search": "查找规则链", "select-rulechain": "选择规则链", - "selected-rulechains": "已选择 { count, plural, 1 {1 个规则链} other {# 个规则链} }", + "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }", "set-root": "设置为根规则链", "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", "set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?", @@ -1961,7 +1961,7 @@ "description": "说明", "deselect-all": "取消选择", "deselect-all-objects": "取消选择所有节点和连接", - "details": "细节", + "details": "详情", "directive-is-not-loaded": "定义的配置指令 '{{directiveName}}' 不可用。", "events": "事件", "help": "帮助", @@ -1985,7 +1985,7 @@ "output": "输出", "reset-debug-mode": "重置所有节点中的调试模式", "rulenode-details": "规则节点详情", - "search": "搜索节点", + "search": "查找节点", "select-all": "选择全部", "select-all-objects": "选择所有节点和连接", "select-message-type": "选择消息类型", @@ -1998,14 +1998,14 @@ "type-enrichment-details": "向消息元数据中添加附加信息", "type-external": "外部的", "type-external-details": "与外部系统交互", - "type-filter": "过滤器", - "type-filter-details": "使用配置条件过滤传入消息", + "type-filter": "筛选器", + "type-filter-details": "使用配置条件筛选传入消息", "type-input": "输入", "type-input-details": "规则链的逻辑输入,将传入消息转发到下一个相关规则节点", "type-rule-chain": "规则链", "type-rule-chain-details": "将传入消息转发到指定的规则链", "type-transformation": "变换", - "type-transformation-details": "更改消息有效载荷和元数据", + "type-transformation-details": "更改消息 Payload 和元数据", "type-unknown": "未知", "type-unknown-details": "未解析的规则节点", "ui-resources-load-error": "加载配置UI资源失败。" @@ -2016,20 +2016,20 @@ "create-new-tenant-profile": "创建一个新的!", "data": "纵断面数据", "default": "默认", - "default-storage-ttl-days": "默认存储TTL天(0-无限制)", + "default-storage-ttl-days": "默认存储 TTL 天数(0-无限制)", "default-storage-ttl-days-range": "不能为负存储天数", "default-storage-ttl-days-required": "默认存储 TTL 天数必填。", "delete": "删除租户配置", "delete-tenant-profile-text": "请注意:确认后,租户配置和所有相关数据将不可。", "delete-tenant-profile-title": "确实要删除租户配置 '{{tenantProfileName}}'吗?", "delete-tenant-profiles-text": "请注意:确认后,所有选定的租户配置将被删除,所有相关数据将不可恢复。", - "delete-tenant-profiles-title": "确定要删除 { count, plural, 1 {1 个租户配置} other {# 个租户配置} }?", + "delete-tenant-profiles-title": "确定要删除 { count, plural, 1 {# 个租户配置} other {# 个租户配置} }?", "description": "说明", "edit": "编辑租户配置", "idCopiedMessage": "租户配置Id已复制到剪贴板", - "max-d-p-storage-days": "最大数据点存储天数(0-无限制)", - "max-d-p-storage-days-range": "最大数据点存储天数不能为负数", - "max-d-p-storage-days-required": "最大数据点存储天数必填。", + "max-d-p-storage-days": "最大日存储数据点数(0-无限制)", + "max-d-p-storage-days-range": "最大日存储数据点数不能为负数", + "max-d-p-storage-days-required": "最大日存储数据点数必填。", "max-emails": "发送的最大电子邮件数(0-无限制)", "max-emails-range": "发送的最大电子邮件数不能为负数", "max-emails-required": "最大电子邮件发送数必填。", @@ -2075,8 +2075,8 @@ "no-tenant-profiles-matching": "找不到与 '{{entity}}' 匹配的租户配置。", "no-tenant-profiles-text": "找不到租户配置", "profile-configuration": "配置设置", - "search": "搜索租户配置", - "selected-tenant-profiles": "已选择 { count, plural, 1 {1 个租户配置} other {# 个租户配置} }", + "search": "查找租户配置", + "selected-tenant-profiles": "已选择 { count, plural, 1 {# 个租户配置} other {# 个租户配置} }", "set-default": "设置该租户配置为默认", "set-default-tenant-profile-text": "确认后,此租户配置将被标记为默认配置,并将用于未指定配置的新租户。", "set-default-tenant-profile-title": "确实要将租户配置 '{{tenantProfileName}}' 设为默认值吗?", @@ -2099,11 +2099,11 @@ "delete": "删除租户", "delete-tenant-text": "小心!确认后,租户和所有相关数据将不可恢复。", "delete-tenant-title": "您确定要删除租户'{{tenantTitle}}'吗?", - "delete-tenants-action-title": "删除 { count, plural, 1 {1 个租户} other {# 个租户} }", + "delete-tenants-action-title": "删除 { count, plural, 1 {# 个租户} other {# 个租户} }", "delete-tenants-text": "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。", - "delete-tenants-title": "确定要删除 {count,plural,1 {1 个租户} other {# 个租户} } 吗?", + "delete-tenants-title": "确定要删除 {count,plural,1 {# 个租户} other {# 个租户} } 吗?", "description": "说明", - "details": "细节", + "details": "详情", "events": "事件", "idCopiedMessage": "租户ID已经复制到粘贴板", "isolated-tb-core": "隔离板芯容器中的加工", @@ -2114,9 +2114,9 @@ "management": "租户管理", "no-tenants-matching": "没有找到符合 '{{entity}}' 的租户", "no-tenants-text": "没有找到租户", - "search": "搜索租户", + "search": "查找租户", "select-tenant": "选择租户", - "selected-tenants": "已选择 { count, plural, 1 {1 个租户} other {# 个租户} }", + "selected-tenants": "已选择 { count, plural, 1 {# 个租户} other {# 个租户} }", "tenant": "租户", "tenant-details": "租客详情", "tenant-required": "租户必填", @@ -2127,13 +2127,13 @@ "timeinterval": { "advanced": "高级", "days": "天", - "days-interval": "{ days, plural, 1 {1 天} other {# 天} }", + "days-interval": "{ days, plural, 1 {# 天} other {# 天} }", "hours": "小时", - "hours-interval": "{ hours, plural, 1 {1 小时} other {# 小时} }", + "hours-interval": "{ hours, plural, 1 {# 小时} other {# 小时} }", "minutes": "分钟", - "minutes-interval": "{ minutes, plural, 1 {1 分} other {# 分} }", + "minutes-interval": "{ minutes, plural, 1 {# 分} other {# 分} }", "seconds": "秒", - "seconds-interval": "{ seconds, plural, 1 {1 秒} other {# 秒} }" + "seconds-interval": "{ seconds, plural, 1 {# 秒} other {# 秒} }" }, "timeunit": { "days": "天", @@ -2143,17 +2143,17 @@ }, "timewindow": { "date-range": "日期范围", - "days": "{ days, plural, 1 { 天 } other {# 天 } }", + "days": "{ days, plural, 1 {# 天 } other {# 天 } }", "edit": "编辑时间窗口", "hide": "隐藏", "history": "历史", - "hours": "{ hours, plural, 0 { 小时 } 1 {1 小时 } other {# 小时 } }", + "hours": "{ hours, plural, 0 {- 小时 } 1 {# 小时 } other {# 小时 } }", "last": "最后", "last-prefix": "最后", - "minutes": "{ minutes, plural, 0 { 分 } 1 {1 分 } other {# 分 } }", + "minutes": "{ minutes, plural, 0 {- 分 } 1 {# 分 } other {# 分 } }", "period": "从 {{ startTime }} 到 {{ endTime }}", "realtime": "实时", - "seconds": "{ seconds, plural, 0 { 秒 } 1 {1 秒 } other {# 秒 } }", + "seconds": "{ seconds, plural, 0 {- 秒 } 1 {# 秒 } other {# 秒 } }", "time-period": "时间段" }, "timezone": { @@ -2179,11 +2179,11 @@ "delete": "删除用户", "delete-user-text": "小心!确认后,用户和所有相关数据将不可恢复。", "delete-user-title": "您确定要删除用户 '{{userEmail}}' 吗?", - "delete-users-action-title": "删除 { count, plural, 1 {1 个用户} other {# 个用户} }", + "delete-users-action-title": "删除 { count, plural, 1 {# 个用户} other {# 个用户} }", "delete-users-text": "小心!确认后,所有选定的用户将被删除,所有相关数据将不可恢复。", - "delete-users-title": "确定要删除 { count, plural, 1 {1 个用户} other {# 个用户} } 吗?", + "delete-users-title": "确定要删除 { count, plural, 1 {# 个用户} other {# 个用户} } 吗?", "description": "说明", - "details": "细节", + "details": "详情", "disable-account": "禁用用户帐户", "disable-account-message": "已成功禁用用户帐户!", "display-activation-link": "显示激活链接", @@ -2199,9 +2199,9 @@ "no-users-matching": "没有找到符合 '{{entity}}' 的用户。", "no-users-text": "找不到用户", "resend-activation": "重新发送激活", - "search": "搜索用户", + "search": "查找用户", "select-user": "选择用户", - "selected-users": "已选择 { count, plural, 1 {1 个用户} other {# 个用户} }", + "selected-users": "已选择 { count, plural, 1 {# 个用户} other {# 个用户} }", "send-activation-mail": "发送激活邮件", "sys-admin": "系统管理员", "tenant-admin": "租户管理员", @@ -2277,12 +2277,12 @@ "drop-shadow": "阴影", "edit-action": "编辑 Action", "enable-fullscreen": "启用全屏", - "general-settings": "常规设置", + "general-settings": "基本设置", "height": "高度", "icon-color": "图标颜色", "icon-size": "图标大小", "margin": "边缘", - "maximum-datasources": "最大允许 { count, plural, 1 {1 个数据源。} other {# 个数据源。} }", + "maximum-datasources": "最大允许 { count, plural, 1 {# 个数据源。} other {# 个数据源。} }", "mobile-mode-settings": "移动端设置", "no-actions-text": "找不到 Action", "order": "顺序", @@ -2344,7 +2344,7 @@ "save-widget-type-as": "部件类型另存为", "save-widget-type-as-text": "请输入新的部件标题或选择目标部件组", "saveAs": "部件另存为", - "search-data": "搜索数据", + "search-data": "查找数据", "select-widget-type": "选择窗口部件类型", "select-widgets-bundle": "选择部件组", "settings-schema": "设置模式", @@ -2373,10 +2373,10 @@ "delete": "删除部件组", "delete-widgets-bundle-text": "小心!确认后,部件组和所有相关数据将不可恢复。", "delete-widgets-bundle-title": "您确定要删除部件组 '{{widgetsBundleTitle}}'吗?", - "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {1 个部件组} other {# 个部件组} }", + "delete-widgets-bundles-action-title": "删除 { count, plural, 1 {# 个部件组} other {# 个部件组} }", "delete-widgets-bundles-text": "小心!确认后,所有选定的部件组将被删除,所有相关数据将不可恢复。", - "delete-widgets-bundles-title": "确定要删除 { count, plural, 1 {1 个部件组} other {# 个部件组} } 吗?", - "details": "细节", + "delete-widgets-bundles-title": "确定要删除 { count, plural, 1 {# 个部件组} other {# 个部件组} } 吗?", + "details": "详情", "empty": "部件组是空的", "export": "导出部件组", "export-failed-error": "无法导出部件组: {{error}}", @@ -2385,8 +2385,8 @@ "no-widgets-bundles-matching": "没有找到与 '{{widgetsBundle}}' 匹配的部件组。", "no-widgets-bundles-text": "找不到部件组", "open-widgets-bundle": "打开部件组", - "search": "搜索部件组", - "selected-widgets-bundles": "已选择 { count, plural, 1 {1 个部件组} other {# 个部件组} }", + "search": "查找部件组", + "selected-widgets-bundles": "已选择 { count, plural, 1 {# 个部件组} other {# 个部件组} }", "system": "系统", "title": "标题", "title-required": "标题必填。", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index dcdc0909c0..b1c5167ac9 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1118,6 +1118,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@7.10.4", "@babel/template@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -6527,6 +6534,13 @@ ngx-daterangepicker-material@^4.0.1: dependencies: tslib "^1.10.0" +ngx-drag-drop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ngx-drag-drop/-/ngx-drag-drop-2.0.0.tgz#65d970229964803726fb7b9af4aec24005c810c7" + integrity sha512-t+4/eiC8zaXKqU1ruNfFEfGs1GpMNwpffD0baopvZFKjQHCb5rhNqFilJ54wO4T0OwGp4/RnsVhlcxe1mX6UJg== + dependencies: + tslib "^1.9.0" + "ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": version "0.0.0" resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278" @@ -7941,7 +7955,7 @@ rc-util@^4.15.3: react-lifecycles-compat "^3.0.4" shallowequal "^1.1.0" -rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.3.0: +rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004" integrity sha512-kXDn1JyLJTAWLBFt+fjkTcUtXhxKkipQCobQmxIEVrX62iXgo24z8YKoWehWfMxPZFPE+RXqrmEu9j5kHz/Lrg== @@ -7949,6 +7963,15 @@ rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.3.0: react-is "^16.12.0" shallowequal "^1.1.0" +rc-util@^5.0.6: + version "5.7.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.7.0.tgz#776b14cf5bbfc24f419fd40c42ffadddda0718fc" + integrity sha512-0hh5XkJ+vBDeMJsHElqT1ijMx+gC3gpClwQ10h/5hccrrgrMx8VUem183KLlH1YrWCfMMPmDXWWNnwsn+p6URw== + dependencies: + "@babel/runtime" "^7.12.5" + react-is "^16.12.0" + shallowequal "^1.1.0" + rc-virtual-list@^1.1.2: version "1.1.6" resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.6.tgz#b255baf9aacde149a8893324e6307214094f4c0a"