diff --git a/.travis.yml b/.travis.yml
index 6f22da9139..f635da5a55 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,9 +4,9 @@ before_install:
- export MAVEN_OPTS="-Dmaven.repo.local=$HOME/.m2/repository -Xms1024m -Xmx3072m"
- export HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false
jdk:
- - oraclejdk8
+ - openjdk8
language: java
sudo: required
services:
- docker
-script: mvn clean verify -Ddockerfile.skip=false -DblackBoxTests.skip=false -DblackBoxTests.skipTailChildContainers=true
+script: mvn clean verify -Ddockerfile.skip=false
diff --git a/application/pom.xml b/application/pom.xml
index 68ae1aa77c..19e1c5c759 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
thingsboard
application
diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
index d811393c4b..2daf3e27c8 100644
--- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
@@ -16,10 +16,10 @@
"templateHtml": "\n ",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\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}\n",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStatusFilter\": {\n \"title\": \"Enable alarm status filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStatusFilter\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\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, alarm, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStatusFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}"
}
}
]
-}
\ No newline at end of file
+}
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 dab5e7976e..d07186fd42 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -15,7 +15,7 @@
"resources": [],
"templateHtml": "",
"templateCss": "#container {\n overflow: auto;\n}\n\n.tbDatasource-container {\n margin: 5px;\n padding: 8px;\n}\n\n.tbDatasource-title {\n font-size: 1.200rem;\n font-weight: 500;\n padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n width: 100%;\n box-shadow: 0 0 10px #ccc;\n border-collapse: collapse;\n white-space: nowrap;\n font-size: 1.000rem;\n color: #757575;\n}\n\n.tbDatasource-table td {\n position: relative;\n border-top: 1px solid rgba(0, 0, 0, 0.12);\n border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n padding: 0px 18px;\n box-sizing: border-box;\n}",
- "controllerScript": "self.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \" \");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasoirceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasoirceTitleFontSize = self.ctx.width/12;\n }\n datasoirceTitleFontSize = Math.min(datasoirceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasoirceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n \n self.ctx.datasourceTitleCells = [];\n self.ctx.valueCells = [];\n self.ctx.labelCells = [];\n \n for (var i=0; i < self.ctx.datasources.length; i++) {\n var tbDatasource = self.ctx.datasources[i];\n\n var datasourceId = 'tbDatasource' + i;\n self.ctx.$container.append(\n \"
\"\n );\n\n var datasourceContainer = $('#' + datasourceId,\n self.ctx.$container);\n\n datasourceContainer.append(\n \"\" +\n tbDatasource.name + \"
\"\n );\n \n var datasourceTitleCell = $('.tbDatasource-title', datasourceContainer);\n self.ctx.datasourceTitleCells.push(datasourceTitleCell);\n \n var tableId = 'table' + i;\n datasourceContainer.append(\n \"\"\n );\n var table = $('#' + tableId, self.ctx.$container);\n\n for (var a = 0; a < tbDatasource.dataKeys.length; a++) {\n var dataKey = tbDatasource.dataKeys[a];\n var labelCellId = 'labelCell' + a;\n var cellId = 'cell' + a;\n table.append(\"\" + dataKey.label +\n \" \");\n var labelCell = $('#' + labelCellId, table);\n self.ctx.labelCells.push(labelCell);\n var valueCell = $('#' + cellId, table);\n self.ctx.valueCells.push(valueCell);\n }\n } \n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.valueCells.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData && cellData.data && cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length -\n 1];\n var value = tvPair[1];\n var textValue;\n //toDo -> + IsNumber\n \n if (isNumber(value)) {\n var decimals = self.ctx.decimals;\n var units = self.ctx.units;\n if (cellData.dataKey.decimals || cellData.dataKey.decimals === 0) {\n decimals = cellData.dataKey.decimals;\n }\n if (cellData.dataKey.units) {\n units = cellData.dataKey.units;\n }\n txtValue = self.ctx.utils.formatValue(value, decimals, units, true);\n } else {\n txtValue = value;\n }\n self.ctx.valueCells[i].html(txtValue);\n }\n }\n \n function isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n }\n}\n\nself.onResize = function() {\n var datasourceTitleFontSize = self.ctx.height/8;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n datasourceTitleFontSize = self.ctx.width/12;\n }\n datasourceTitleFontSize = Math.min(datasourceTitleFontSize, 20);\n for (var i = 0; i < self.ctx.datasourceTitleCells.length; i++) {\n self.ctx.datasourceTitleCells[i].css('font-size', datasourceTitleFontSize+'px');\n }\n var valueFontSize = self.ctx.height/9;\n var labelFontSize = self.ctx.height/9;\n if (self.ctx.width/self.ctx.height <= 1.5) {\n valueFontSize = self.ctx.width/15;\n labelFontSize = self.ctx.width/15;\n }\n valueFontSize = Math.min(valueFontSize, 18);\n labelFontSize = Math.min(labelFontSize, 18);\n\n for (i = 0; i < self.ctx.valueCells; i++) {\n self.ctx.valueCells[i].css('font-size', valueFontSize+'px');\n self.ctx.valueCells[i].css('height', valueFontSize*2.5+'px');\n self.ctx.valueCells[i].css('padding', '0px ' + valueFontSize + 'px');\n self.ctx.labelCells[i].css('font-size', labelFontSize+'px');\n self.ctx.labelCells[i].css('height', labelFontSize*2.5+'px');\n self.ctx.labelCells[i].css('padding', '0px ' + labelFontSize + 'px');\n } \n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Attributes card\"}"
@@ -32,9 +32,9 @@
"templateHtml": "\n ",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: 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 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\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, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}]}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"enableSelectColumnDisplay\":true},\"title\":\"Entity table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{}}"
}
},
{
@@ -63,7 +63,7 @@
"resources": [],
"templateHtml": "",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml));\n self.ctx.html = '' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n}\n\n",
+ "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n var evtFnPrefix = 'htmlValueCard_' + Math.abs(hashCode(self.ctx.settings.cardCss + self.ctx.settings.cardHtml));\n self.ctx.html = '' + \n self.ctx.settings.cardHtml + \n '
';\n\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n window[evtFnPrefix + '_onClickFn'] = function (event) {\n self.ctx.actionsApi.elementClick(event);\n }\n\n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label == 'entityLabel') {\n variableInfo.isEntityLabel = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && !variableInfo.isEntityLabel && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var $injector = self.ctx.$scope.$injector;\n var utils = $injector.get('utils');\n var types = $injector.get('types');\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n } else if (variableInfo.isEntityLabel) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityLabel || self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n text = replaceCustomTranslations(text);\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n \n function replaceCustomTranslations (pattern) {\n var customTranslationRegex = new RegExp('{' + types.translate.i18nPrefix + ':[^{}]+}', 'g');\n pattern = pattern.replace(customTranslationRegex, getTranslationText);\n return pattern;\n }\n \n function getTranslationText (variable) {\n return utils.customTranslation(variable, variable);\n \n }\n}\n\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"cardHtml\"],\n \"properties\": {\n \"cardCss\": {\n \"title\": \"CSS\",\n \"type\": \"string\",\n \"default\": \".card {\\n font-weight: bold; \\n}\"\n },\n \"cardHtml\": {\n \"title\": \"HTML\",\n \"type\": \"string\",\n \"default\": \"HTML code here
\"\n }\n }\n },\n \"form\": [\n {\n \"key\": \"cardCss\",\n \"type\": \"css\"\n }, \n {\n \"key\": \"cardHtml\",\n \"type\": \"html\"\n } \n ]\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"\\n
\\n
\\n
Value title \\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n
\\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
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 2ecd90be28..9c97a03084 100644
--- a/application/src/main/data/json/system/widget_bundles/charts.json
+++ b/application/src/main/data/json/system/widget_bundles/charts.json
@@ -35,7 +35,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.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);\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.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}"
@@ -147,7 +147,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.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);\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\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",
"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\":{}}"
@@ -163,7 +163,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.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);\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.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\":\"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\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json
index 5dfe4d0f7f..44f7d52ebf 100644
--- a/application/src/main/data/json/system/widget_bundles/control_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json
@@ -109,9 +109,9 @@
"sizeX": 4,
"sizeY": 2,
"resources": [],
- "templateHtml": "",
+ "templateHtml": "",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
- "controllerScript": "self.onInit = function() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n }\n );\n };\n\n};",
+ "controllerScript": "self.onInit = function() {\n let rpcEnabled = self.ctx.defaultSubscription.rpcEnabled;\n\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton.bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n if (!rpcEnabled) {\n self.ctx.$scope.error =\n 'Target device is not set!';\n }\n\n self.ctx.$scope.sendCommand = function() {\n var rpcMethod = self.ctx.settings.methodName;\n var rpcParams = self.ctx.settings.methodParams;\n var timeout = self.ctx.settings.requestTimeout;\n var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ?\n true : false;\n\n var commandPromise;\n if (oneWayElseTwoWay) {\n commandPromise = self.ctx.controlApi.sendOneWayCommand(\n rpcMethod, rpcParams, timeout);\n } else {\n commandPromise = self.ctx.controlApi.sendTwoWayCommand(\n rpcMethod, rpcParams, timeout);\n }\n commandPromise.then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n }\n );\n };\n\n};",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Send RPC\"\n },\n \"oneWayElseTwoWay\": {\n \"title\": \"Is One Way Command\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showError\": {\n \"title\": \"Show RPC command execution error\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"methodName\": {\n \"title\": \"RPC method\",\n \"type\": \"string\",\n \"default\": \"rpcCommand\"\n },\n \"methodParams\": {\n \"title\": \"RPC method params\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 5000\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n \"oneWayElseTwoWay\",\n \"showError\",\n \"methodName\",\n {\n \"key\": \"methodParams\",\n \"type\": \"json\"\n },\n \"requestTimeout\",\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"requestTimeout\":5000,\"oneWayElseTwoWay\":true,\"buttonText\":\"Send RPC\",\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"methodName\":\"rpcCommand\",\"methodParams\":\"{}\"},\"title\":\"RPC Button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -125,9 +125,9 @@
"sizeX": 4,
"sizeY": 2,
"resources": [],
- "templateHtml": "",
+ "templateHtml": "",
"templateCss": ".tb-rpc-button {\n width: 100%;\n height: 100%;\n}\n\n.tb-rpc-button .title-container {\n font-weight: 500;\n white-space: nowrap;\n margin: 10px 0;\n}\n\n.tb-rpc-button .button-container div{\n min-width: 80%\n}\n\n.tb-rpc-button .button-container .md-button{\n width: 100%;\n margin: 0;\n}\n\n.tb-rpc-button .error-container {\n position: absolute;\n top: 2%;\n right: 0;\n left: 0;\n z-index: 4;\n height: 14px;\n}\n\n.tb-rpc-button .error-container .button-error {\n color: #ff3315;\n white-space: nowrap;\n}",
- "controllerScript": "self.onInit = function() {\n self.ctx.$scope.buttonLable = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n console.log(self.ctx);\n\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n \n attributeService.saveEntityAttributes(\"DEVICE\", self.ctx.defaultSubscription.targetDeviceId,\n entityAttributeType, attributes).then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n console.log(rejection);\n }\n\n );\n };\n\n};",
+ "controllerScript": "self.onInit = function() {\n self.ctx.$scope.buttonLabel = self.ctx.settings.buttonText;\n self.ctx.$scope.showTitle = self.ctx.settings.title &&\n self.ctx.settings.title.length ? true : false;\n self.ctx.$scope.title = self.ctx.settings.title;\n self.ctx.$scope.styleButton = self.ctx.settings.styleButton;\n let entityAttributeType = self.ctx.settings.entityAttributeType;\n let entityParameters = JSON.parse(self.ctx.settings.entityParameters);\n\n if (self.ctx.settings.styleButton.isPrimary ===\n false) {\n self.ctx.$scope.customStyle = {\n 'background-color': self.ctx.$scope.styleButton\n .bgColor,\n 'color': self.ctx.$scope.styleButton.textColor\n };\n }\n\n console.log(self.ctx);\n\n let attributeService = self.ctx.$scope.$injector.get('attributeService');\n\n self.ctx.$scope.sendUpdate = function() {\n let attributes = [];\n for (let key in entityParameters) {\n attributes.push({\n \"key\": key,\n \"value\": entityParameters[key]\n });\n }\n \n \n attributeService.saveEntityAttributes(\"DEVICE\", self.ctx.defaultSubscription.targetDeviceId,\n entityAttributeType, attributes).then(\n function success() {\n self.ctx.$scope.error = \"\";\n },\n function fail(rejection) {\n if (self.ctx.settings.showError) {\n self.ctx.$scope.error =\n rejection.status + \": \" +\n rejection.statusText;\n }\n console.log(rejection);\n }\n\n );\n };\n\n};",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"title\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"buttonText\": {\n \"title\": \"Button label\",\n \"type\": \"string\",\n \"default\": \"Update device attribute\"\n },\n \"entityAttributeType\": {\n \"title\": \"Device attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"entityParameters\": {\n \"title\": \"Device attribute parameters\",\n \"type\": \"string\",\n \"default\": \"{}\"\n },\n \"styleButton\": {\n \"type\": \"object\",\n \"title\": \"Button Style\",\n \"properties\": {\n \"isRaised\": {\n \"type\": \"boolean\",\n \"title\": \"Raised\",\n \"default\": true\n },\n \"isPrimary\": {\n \"type\": \"boolean\",\n \"title\": \"Primary color\",\n \"default\": false\n },\n \"bgColor\": {\n \"type\": \"string\",\n \"title\": \"Button background color\",\n \"default\": null\n },\n \"textColor\": {\n \"type\": \"string\",\n \"title\": \"Button text color\",\n \"default\": null\n }\n }\n },\n \"required\": []\n }\n },\n \"form\": [\n \"title\",\n \"buttonText\",\n {\n \"key\": \"entityAttributeType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [{\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n }, {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }]\n }, {\n \"key\": \"entityParameters\",\n \"type\": \"json\"\n },\n {\n \"key\": \"styleButton\",\n \"items\": [\n \"styleButton.isRaised\",\n \"styleButton.isPrimary\",\n {\n \"key\": \"styleButton.bgColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"styleButton.textColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}"
diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
index d9490e0c3b..c0d0fa7cdf 100644
--- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
@@ -16,9 +16,9 @@
"templateHtml": "\n ",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: 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}\n",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\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, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\"},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddDeviceDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addDeviceForm.$setPristine();\\n let device = {\\n name: vm.deviceName,\\n type: vm.deviceType,\\n label: vm.deviceLabel\\n };\\n deviceService.saveDevice(device).then(\\n (device) => {\\n saveAttributes(device.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditDeviceDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n deviceService.getDevice(entityId.id).then(\\n (device) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.device = device;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editDeviceForm.$setPristine();\\n deviceService.saveDevice(vm.device).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteDevice();\\n })\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"device\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddDeviceDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addDeviceForm.$setPristine();\\n let device = {\\n name: vm.deviceName,\\n type: vm.deviceType,\\n label: vm.deviceLabel\\n };\\n deviceService.saveDevice(device).then(\\n (device) => {\\n saveAttributes(device.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditDeviceDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditDeviceDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n deviceService.getDevice(entityId.id).then(\\n (device) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.device = device;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editDeviceForm.$setPristine();\\n deviceService.saveDevice(vm.device).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n deviceService = $injector.get('deviceService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteDevice();\\n })\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}"
}
},
{
@@ -32,10 +32,10 @@
"templateHtml": "\n ",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: 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}\n",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\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 \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\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, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\"},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddAssetDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addAssetForm.$setPristine();\\n let asset = {\\n name: vm.assetName,\\n type: vm.assetType\\n };\\n assetService.saveAsset(asset).then(\\n (asset) => {\\n saveAttributes(asset.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditAssetDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n assetService.getAsset(entityId.id).then(\\n (asset) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.asset = asset;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editAssetForm.$setPristine();\\n assetService.saveAsset(vm.asset).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteAsset();\\n })\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"asset\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\",\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', AddAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction AddAssetDialogController($scope, $mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.attributes = {};\\n \\n vm.cancel = () => {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.addAssetForm.$setPristine();\\n let asset = {\\n name: vm.assetName,\\n type: vm.assetType\\n };\\n assetService.saveAsset(asset).then(\\n (asset) => {\\n saveAttributes(asset.id).then(\\n () => {\\n vm.loading = false;\\n updateAliasData();\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\"}],\"actionCellButton\":[{\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\",\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n \\n \\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n $q = $injector.get('$q'),\\n $rootScope = $injector.get('$rootScope'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n attributeService = $injector.get('attributeService');\\n \\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n $mdDialog.show({\\n controller: ['$scope','$mdDialog', EditAssetDialogController],\\n controllerAs: 'vm',\\n template: htmlTemplate,\\n parent: angular.element($document[0].body),\\n targetEvent: $event,\\n multiple: true,\\n clickOutsideToClose: false\\n });\\n}\\n\\nfunction EditAssetDialogController($scope,$mdDialog) {\\n let vm = this;\\n vm.types = types;\\n vm.loading = false;\\n vm.attributes = {};\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n vm.loading = true;\\n assetService.getAsset(entityId.id).then(\\n (asset) => {\\n attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\\n (data) => {\\n if (data.length) {\\n getEntityAttributes(data);\\n }\\n vm.asset = asset;\\n vm.loading = false;\\n } \\n );\\n }\\n )\\n }\\n \\n vm.cancel = function() {\\n $mdDialog.hide();\\n };\\n \\n vm.save = () => {\\n vm.loading = true;\\n $scope.editAssetForm.$setPristine();\\n assetService.saveAsset(vm.asset).then(\\n () => {\\n saveAttributes().then(\\n () => {\\n updateAliasData();\\n vm.loading = false;\\n $mdDialog.hide();\\n }\\n );\\n },\\n () => {\\n vm.loading = false;\\n }\\n );\\n }\\n \\n function getEntityAttributes(attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n }\\n \\n function saveAttributes() {\\n let attributesArray = [];\\n for (let key in vm.attributes) {\\n attributesArray.push({key: key, value: vm.attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return $q.when([]);\\n }\\n }\\n \\n function updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n console.log(widgetContext);\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\\n }\\n}\\n\"},{\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\",\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet $mdDialog = $injector.get('$mdDialog'),\\n $document = $injector.get('$document'),\\n types = $injector.get('types'),\\n assetService = $injector.get('assetService'),\\n $rootScope = $injector.get('$rootScope'),\\n $q = $injector.get('$q');\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n let confirm = $mdDialog.confirm()\\n .targetEvent($event)\\n .title(title)\\n .htmlContent(content)\\n .ariaLabel(title)\\n .cancel('Cancel')\\n .ok('Delete');\\n $mdDialog.show(confirm).then(() => {\\n deleteAsset();\\n })\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).then(\\n () => {\\n updateAliasData();\\n }\\n );\\n}\\n\\nfunction updateAliasData() {\\n let aliasIds = [];\\n for (let id in widgetContext.aliasController.resolvedAliases) {\\n aliasIds.push(id);\\n }\\n let tasks = [];\\n aliasIds.forEach((aliasId) => {\\n widgetContext.aliasController.setAliasUnresolved(aliasId);\\n tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\\n });\\n $q.all(tasks).then(() => {\\n $rootScope.$broadcast('entityAliasesChanged', aliasIds);\\n });\\n}\"}]}}"
}
}
]
-}
\ No newline at end of file
+}
diff --git a/application/src/main/data/json/system/widget_bundles/input_widgets.json b/application/src/main/data/json/system/widget_bundles/input_widgets.json
index 5129ea19a7..2cc297b263 100644
--- a/application/src/main/data/json/system/widget_bundles/input_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/input_widgets.json
@@ -101,6 +101,22 @@
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
},
+ {
+ "alias": "update_server_location_attribute",
+ "name": "Update server location attribute",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 3,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width md-input-container{\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}",
+ "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n console.log(self.ctx);\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.attribute){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function() {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [{\r\n key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function() {\r\n if ($scope.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update server location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+ }
+ },
{
"alias": "update_shared_string_attribute",
"name": "Update shared string attribute",
@@ -197,6 +213,22 @@
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared image attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
},
+ {
+ "alias": "update_shared_location_attribute",
+ "name": "Update shared location attribute",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 3,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width md-input-container{\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}",
+ "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n console.log(self.ctx);\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\r\n $scope.message = $translate.instant('widgets.input-widgets.no-entity-selected');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType === types.entityType.device) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n \r\n $scope.entityDetected = true;\r\n }\r\n } else {\r\n $scope.message = $translate.instant('widgets.input-widgets.not-allowed-entity');\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.attribute){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function() {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityAttributes(\r\n datasource.entityType,\r\n datasource.entityId,\r\n types.attributesScope.server.value,\r\n [{\r\n key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function() {\r\n if ($scope.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update shared location attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+ }
+ },
{
"alias": "update_string_timeseries",
"name": "Update string timeseries",
@@ -261,6 +293,22 @@
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
},
+ {
+ "alias": "update_location_timeseries",
+ "name": "Update location timeseries",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 3,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.entity-title {\n font-weight: bold;\n font-size: 22px;\n padding-top: 12px;\n padding-bottom: 6px;\n color: #666;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex-direction: column;\n flex: 1;\n}\n\n.grid__element.horizontal-alignment {\n flex-direction: row;\n}\n\n.grid__element:last-child {\n align-items: center;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n margin: 0;\n}\n\n.attribute-update-form .md-button.md-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0;\n line-height: 20px;\n}\n\n.attribute-update-form .md-button.getLocation {\n margin-right: 10px;\n}\n\n.attribute-update-form .md-icon-button md-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.attribute-update-form md-input-container{\n width: 100%;\n margin: 18px 0 5px;\n}\n\n.attribute-update-form.small-width md-input-container{\n width: 150px;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}\n\nmd-toast{\n min-width: 0;\n}\nmd-toast .md-toast-content {\n font-size: 14px!important;\n}",
+ "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $translate;\r\n\r\nself.onInit = function() {\r\n console.log(self.ctx);\r\n $scope = self.ctx.$scope;\r\n attributeService = $scope.$injector.get('attributeService');\r\n toast = $scope.$injector.get('toast');\r\n utils = $scope.$injector.get('utils');\r\n types = $scope.$injector.get('types');\r\n $translate = $scope.$injector.get('$translate');\r\n settings = self.ctx.settings || {};\r\n $scope.settings = settings;\r\n $scope.isValidParameter = true;\r\n $scope.dataKeyDetected = false;\r\n $scope.isHorizontal = (settings.inputFieldsAlignment === 'row') ? true : false;\r\n $scope.requiredErrorMessage = utils.customTranslation(settings.requiredErrorMessage, settings.requiredErrorMessage) || $translate.instant('widgets.input-widgets.entity-coordinate-required');\r\n $scope.latLabel = utils.customTranslation(settings.latLabel, settings.latLabel) || $translate.instant('widgets.input-widgets.latitude');\r\n $scope.lngLabel = utils.customTranslation(settings.lngLabel, settings.lngLabel) || $translate.instant('widgets.input-widgets.longitude');\r\n\r\n if (self.ctx.datasources && self.ctx.datasources.length) {\r\n var datasource = self.ctx.datasources[0];\r\n if (datasource.type === types.datasourceType.entity) {\r\n if (datasource.entityType && datasource.entityId) {\r\n $scope.entityName = datasource.entityName;\r\n if (settings.widgetTitle && settings.widgetTitle.length) {\r\n $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\r\n } else {\r\n $scope.titleTemplate = self.ctx.widgetConfig.title;\r\n }\r\n\r\n $scope.entityDetected = true;\r\n }\r\n }\r\n if (datasource.dataKeys.length > 1) {\r\n $scope.dataKeyDetected = true;\r\n for (let i = 0; i < datasource.dataKeys.length; i++) {\r\n if (datasource.dataKeys[i].type != types.dataKeyType.timeseries){\r\n $scope.isValidParameter = false;\r\n }\r\n if (datasource.dataKeys[i].name !== settings.latKeyName && datasource.dataKeys[i].name !== settings.lngKeyName){\r\n $scope.dataKeyDetected = false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\r\n\r\n $scope.updateAttribute = function() {\r\n if ($scope.entityDetected) {\r\n var datasource = self.ctx.datasources[0];\r\n\r\n attributeService.saveEntityTimeseries(\r\n datasource.entityType,\r\n datasource.entityId,\r\n 'scope',\r\n [{\r\n key: settings.latKeyName,\r\n value: $scope.currentLat\r\n },{\r\n key: settings.lngKeyName,\r\n value: $scope.currentLng\r\n }]\r\n ).then(\r\n function success() {\r\n $scope.originalLat = $scope.currentLat;\r\n $scope.originalLng = $scope.currentLng;\r\n if (settings.showResultMessage) {\r\n toast.showSuccess($translate.instant('widgets.input-widgets.update-successful'), 1000,\r\n angular.element(self.ctx.$container), 'bottom left');\r\n }\r\n },\r\n function fail() {\r\n if (settings.showResultMessage) {\r\n toast.showError($translate.instant('widgets.input-widgets.update-failed'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n );\r\n }\r\n };\r\n\r\n $scope.changeFocus = function() {\r\n if ($scope.currentLat === $scope.originalLat || $scope.currentLng ===$scope.originalLng) {\r\n $scope.isFocused = false;\r\n }\r\n }\r\n\r\n $scope.discardChange = function() {\r\n $scope.currentLat = $scope.originalLat;\r\n $scope.currentLng = $scope.originalLng;\r\n scope.isFocused = false;\r\n }\r\n \r\n $scope.disableButton = function () {\r\n return $scope.currentLat === $scope.originalLat && $scope.currentLng === $scope.originalLng || $scope.currentLng === null || $scope.currentLat === null ;\r\n }\r\n \r\n $scope.getCoordinate = function() {\r\n if (navigator.geolocation) {\r\n navigator.geolocation.getCurrentPosition(showPosition, function (){\r\n toast.showError($translate.instant('widgets.input-widgets.blocked-location'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }, {\r\n enableHighAccuracy: settings.enableHighAccuracy\r\n });\r\n } else {\r\n toast.showError($translate.instant('widgets.input-widgets.no-support-geolocation'), \r\n angular.element(self.ctx.$container),'bottom left');\r\n }\r\n }\r\n \r\n function showPosition(position) {\r\n $scope.currentLat = position.coords.latitude;\r\n $scope.currentLng = position.coords.longitude;\r\n }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n try {\r\n if ($scope.dataKeyDetected) {\r\n if (!$scope.isFocused) {\r\n for(let i = 0; i < self.typeParameters().maxDataKeys; i++){\r\n if(self.ctx.data[i].dataKey.name === self.ctx.settings.latKeyName){\r\n $scope.currentLat = $scope.originalLat = self.ctx.data[i].data[0][1];\r\n } else if(self.ctx.data[i].dataKey.name === self.ctx.settings.lngKeyName){\r\n $scope.currentLng = $scope.originalLng = self.ctx.data[i].data[0][1];\r\n }\r\n }\r\n $scope.$digest();\r\n }\r\n }\r\n } catch (e) {\r\n console.log(e);\r\n }\r\n}\r\n\r\nself.onResize = function() {\r\n $scope.smallWidthContainer = (self.ctx.$container[0].offsetWidth < 320) ? true : false;\r\n $scope.changeAlignment = ($scope.isHorizontal && (self.ctx.$container[0].offsetWidth < 480)) ? true : false;\r\n}\r\n\r\nself.typeParameters = function() {\r\n return {\r\n maxDatasources: 1,\r\n maxDataKeys: 2\r\n }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"latKeyName\": {\n \"title\": \"Latitude key name\",\n \"type\": \"string\",\n \"default\": \"latitude\"\n },\n \"lngKeyName\": {\n \"title\": \"Longitude key name\",\n \"type\": \"string\",\n \"default\": \"longitude\"\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"latLabel\": {\n \"title\": \"Label for latitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"lngLabel\": {\n \"title\": \"Label for longitude\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableHighAccuracy\": {\n \"title\": \"Use high accuracy\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showGetLocation\": {\n \"title\": \"Show button 'Get current location'\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"inputFieldsAlignment\": {\n \"title\": \"Input fields alignment\",\n \"type\": \"string\",\n \"default\": \"column\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"latKeyName\",\n \"lngKeyName\",\n \"enableHighAccuracy\",\n \"showGetLocation\",\n \"showResultMessage\",\n {\n \"key\": \"inputFieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"column\",\n \"label\": \"Column (default)\"\n },\n {\n \"value\": \"row\",\n \"label\": \"Row\"\n }\n ]\n },\n \"showLabel\",\n \"latLabel\",\n \"lngLabel\",\n \"requiredErrorMessage\"\n ]\n}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update location timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+ }
+ },
{
"alias": "update_multiple_attributes",
"name": "Update Multiple Attributes",
@@ -271,9 +319,9 @@
"resources": [],
"templateHtml": "\n ",
"templateCss": "",
- "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Multiple input title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributesShared\": {\n \"title\": \"Attributes are 'shared' (default value is 'server')\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\":true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"attributesShared\",\n \"showResultMessage\"\n ]\n}",
- "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"readOnly\": {\n \"title\": \"Value is read only\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"inputTypeNumber\": {\n \"title\": \"Datakey is a number\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between valid values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n },\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 },\n \"required\": []\n },\n \"form\": [\n \"readOnly\",\n \"inputTypeNumber\",\n \"step\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t},\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\n",
+ "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\n\r\nself.onInit = function() {\r\n var scope = self.ctx.$scope;\r\n var id = self.ctx.$scope.$injector.get('utils').guid();\r\n scope.formId = \"form-\"+id;\r\n scope.ctx = self.ctx;\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-data-updated', self.ctx.$scope.formId);\r\n}\r\n\r\nself.onResize = function() {\r\n self.ctx.$scope.$broadcast('multiple-input-resize', self.ctx.$scope.formId);\r\n}\r\n",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified (only if action buttons are visible)\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n \"updateAllValues\",\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n \"fieldsInRow\"\n ]\n}",
+ "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"step\": {\n \"title\": \"Step interval between values (only for numbers)\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n \"required\",\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n \"step\",\n \"requiredErrorMessage\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.23592248334107624,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Update Multiple Attributes\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
},
@@ -292,6 +340,70 @@
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Web Camera Input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
+ },
+ {
+ "alias": "device_claiming_widget",
+ "name": "Device claiming widget",
+ "descriptor": {
+ "type": "static",
+ "sizeX": 7.5,
+ "sizeY": 4.5,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": ".claim-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.show-label label {\n display: block;\n}\n\nlabel {\n display: none;\n}",
+ "controllerScript": "let $scope;\n\nself.onInit = function() {\n $scope = self.ctx.$scope;\n let $injector = $scope.$injector;\n let $q = $injector.get('$q');\n let $http = $injector.get('$http');\n let toast = $scope.$injector.get('toast');\n let utils = $scope.$injector.get('utils');\n let $translate = $scope.$injector.get('$translate');\n let $rootScope = $scope.$injector.get('$rootScope');\n let settings = self.ctx.settings || {};\n $scope.secretKeyField = settings.deviceSecret;\n $scope.showLabel = settings.showLabel;\n $scope.deviceObj = {};\n \n const config = {\n ignoreErrors: true \n };\n \n let titleTemplate = \"\";\n let successfulClaim = utils.customTranslation(settings.successfulClaimDevice, settings.successfulClaimDevice) || $translate.instant('widgets.input-widgets.claim-successful');\n let failedClaimDevice = utils.customTranslation(settings.failedClaimDevice, settings.failedClaimDevice) || $translate.instant('widgets.input-widgets.claim-failed');\n let deviceNotFound = utils.customTranslation(settings.deviceNotFound, settings.deviceNotFound) || $translate.instant('widgets.input-widgets.claim-not-found');\n \n if (settings.widgetTitle && settings.widgetTitle.length) {\n titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n } else {\n titleTemplate = self.ctx.widgetConfig.title;\n }\n self.ctx.widgetTitle = titleTemplate;\n \n $scope.deviceLabel = utils.customTranslation(settings.deviceLabel, settings.deviceLabel) || $translate.instant('widgets.input-widgets.device-name');\n $scope.requiredErrorDevice= utils.customTranslation(settings.requiredErrorDevice, settings.requiredErrorDevice) || $translate.instant('widgets.input-widgets.device-name-required');\n \n $scope.secretKeyLabel = utils.customTranslation(settings.secretKeyLabel, settings.secretKeyLabel) || $translate.instant('widgets.input-widgets.secret-key');\n $scope.requiredErrorSecretKey= utils.customTranslation(settings.requiredErrorSecretKey, settings.requiredErrorSecretKey) || $translate.instant('widgets.input-widgets.secret-key-required');\n \n $scope.labelClaimButon = utils.customTranslation(settings.labelClaimButon, settings.labelClaimButon) || $translate.instant('widgets.input-widgets.claim-device');\n \n $scope.claim = () => {\n $scope.loading = true;\n claimDevice($scope.deviceObj.deviceName, $scope.deviceObj.deviceSecret, config).then(\n (data) => {\n successClaim();\n },\n (error) => {\n $scope.loading = false;\n if(error.status == 404) {\n toast.showError(deviceNotFound, angular.element('.claim-form'),'bottom left');\n } else if(error.status == 400) {\n toast.showError(failedClaimDevice, angular.element('.claim-form'),'bottom left');\n }\n }\n );\n }\n \n function claimDevice(deviceName, deviceSecret, config) {\n let deferred = $q.defer();\n let url = \"/api/customer/device/\" + deviceName + \"/claim\";\n let obj = deviceSecret ? { secretKey: deviceSecret } : {};\n $http.post(url, obj, config).then(\n (payload) => {\n deferred.resolve(payload.data); \n },\n (error) => {\n deferred.reject(error);\n }\n );\n return deferred.promise;\n }\n \n function updateAliasData() {\n var aliasIds = [];\n for (var id in self.ctx.aliasController.resolvedAliases) {\n aliasIds.push(id);\n }\n var tasks = [];\n aliasIds.forEach(function(aliasId) {\n self.ctx.aliasController.setAliasUnresolved(aliasId);\n tasks.push(self.ctx.aliasController.getAliasInfo(aliasId));\n });\n $q.all(tasks).then(function() {\n $rootScope.$broadcast('widgetForceReInit');\n });\n }\n \n function successClaim() {\n resetForm();\n $scope.claimDeviceForm.$setPristine();\n $scope.claimDeviceForm.$setUntouched();\n $scope.loading = false;\n toast.showSuccess(successfulClaim, 2000);\n updateAliasData();\n }\n \n function resetForm() {\n $scope.deviceObj = {};\n }\n}\n\n",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceSecret\": {\n \"title\": \"Show 'Secret key' input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showLabel\": {\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"deviceLabel\": {\n \"title\": \"Label for device name\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorDevice\": {\n \"title\": \"'Device name required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"secretKeyLabel\": {\n \"title\": \"Label for secret key\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"requiredErrorSecretKey\": {\n \"title\": \"'Secret key required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"labelClaimButon\": {\n \"title\": \"Label for claiming button\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"successfulClaimDevice\": {\n \"title\": \"Text message of successful device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"deviceNotFound\": {\n \"title\": \"Text message when device not found\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"failedClaimDevice\": {\n \"title\": \"Text message of failed device claiming\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n [\n \"widgetTitle\",\n \"labelClaimButon\",\n \"deviceSecret\",\n \"showLabel\",\n \"deviceLabel\",\n \"secretKeyLabel\"\n ],\n [\n \"deviceNotFound\",\n \"failedClaimDevice\",\n \"successfulClaimDevice\",\n \"requiredErrorDevice\",\n \"requiredErrorSecretKey\"\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Message settings\"\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\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"deviceSecret\":true,\"showLabel\":true},\"title\":\"Device claiming widget\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+ }
+ },
+ {
+ "alias": "markers_placement_image_map",
+ "name": "Markers Placement - Image Map",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 8.5,
+ "sizeY": 6.5,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n",
+ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('image-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 0.2;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || 0.3;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"xPos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 0.6;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"yPos\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || 0.7;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName} X Pos: ${xPos:2}Y Pos: ${yPos:2}Delete \",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapImageUrl\":\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB3aWR0aD0iMTEzNC41MTgzIgogICBoZWlnaHQ9Ijc2Mi43ODI0MSIKICAgaWQ9InN2ZzIiCiAgIHZlcnNpb249IjEuMSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMC40OC41IHIxMDA0MCIKICAgc29kaXBvZGk6ZG9jbmFtZT0id2ljaGl0YW1hcC1ub2xpYi5zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM0IiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0iYmFzZSIKICAgICBwYWdlY29sb3I9IiNmZmZmZmYiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgYm9yZGVyb3BhY2l0eT0iMS4wIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp6b29tPSIwLjM1IgogICAgIGlua3NjYXBlOmN4PSI4OS45MDc4NTciCiAgICAgaW5rc2NhcGU6Y3k9IjQ1My43ODI0MSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ibGF5ZXIxIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzNjYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iNzIxIgogICAgIGlua3NjYXBlOndpbmRvdy14PSItNCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpvYmplY3QtcGF0aHM9InRydWUiCiAgICAgaW5rc2NhcGU6c25hcC1nbG9iYWw9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBmaXQtbWFyZ2luLXRvcD0iMCIKICAgICBmaXQtbWFyZ2luLWxlZnQ9IjAiCiAgICAgZml0LW1hcmdpbi1yaWdodD0iMCIKICAgICBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE3Ij4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICAgIDxkYzp0aXRsZT48L2RjOnRpdGxlPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJMYXllciAxIgogICAgIGlua3NjYXBlOmdyb3VwbW9kZT0ibGF5ZXIiCiAgICAgaWQ9ImxheWVyMSIKICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjcuMDcxNDI4LC0zMDcuOTAyOTkpIj4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM3ODciCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzY0ZTU5O3N0cm9rZS13aWR0aDoyLjk5OTk5OTc2O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmUiCiAgICAgICBkPSJtIDkwNi4wMzMxNSw3MDYuMTMzNjcgMy40MjkyLDE3Ljc5NTUyIE0gMjguNTcxNDI4LDc2NS4wNTA2NyBjIDE1MC40MzUyMDIsNi44MzM0MiAxNDYuMzkyMzIyLC0yNi4zMzQxNSAxNjYuNDM0NTQyLC0yOS4zMjAwOSAzNi4xNDM3NSwtNS4zODQ3NiAxMTQuMjg2NzYsLTYuNTI1NCAxNDguMzI1MDgsLTguNjIzNTQgNDMuMzc4MDgsLTIuNjczODUgMTQxLjc2MjIxLC0xMS4yMzA5OSAxODguODU1NzgsLTE5LjgzNDE4IDM5LjgxMTM4LC03LjI3Mjg0IDIyMS4zNjk5MSwtMC44NjIzNSAzMTkuMDcxNDEsLTAuODYyMzUgNzAuODI3MzUsMCAxNDYuOTE4NjcsLTEuNzI0NyAyMTguMTc1ODYsLTEuNzI0NyAtMzEuNjE5NywwIDExNy44NTUyLC0yLjU4NzA3IDg2LjIzNTUsLTIuNTg3MDcgbSAtMjUuMDkwNywtNjguMTI2MDYgYyAtNTIuNzk5NiwzNC43ODQ4NCAtNjUuODk1MSw1MS43NDg2NSAtOTUuNjM5LDgxLjQ5MjU4IC0yNC45MzEzLDI0LjkzMTI3IC0xNDAuMzk2NTMsLTE5LjEzOTIgLTE3OC45Mzg3MSwzNi42NTAwNyAtMTIuMjgxNCwxNy43NzcxNSAtNDcuMDAyNTcsNDYuNTQ2NTMgLTY1LjEwNzgzLDU5LjA3MTMzIC0yMC4xMDUsMTMuOTA4MTggLTU2LjAzNjcyLDQ0Ljk1NjY0IC02Ny43Njg4NSw3My4wNzgyNyAtNC44MDE0NywxMS41MDkwMiAtMTMuMzgwNDYsMzUuOTkyOTggLTIzLjQ0OTQ5LDQ2LjA2MjAxIC0xMC40OTY5OSwxMC40OTY5OSAtMzguMzc3MzMsNi4zODU2OSAtNDQuMDIzNDUsMTcuNjQ3NjQgLTE5LjAwNTAyLDM3LjkwODEyIC0yNS40NjUzLDEwMC45MjM1MiAtNjcuNjE3ODksMTAyLjA1MTAyIG0gMTkuMjgxNTEsLTYyNC4wMTQ2NCBjIDM0LjY1OTM0LC0xLjg3MzgyIDg0LjAyNzMzLDcuMzkxMzEgMTA5LjkwMDcxLC00LjI4NTQ1IDEzLjI4MTcyLC01Ljk5NDA4IDQxLjQwNzIxLC0yLjQ2MTM1IDY2LjgyODY2LC0yLjMyMDQ2IDM1LjMyMjM4LDAuMTk1NzggNjQuMzgyNDksMC42MzQ3NyAxMDEuOTE2Nyw1LjAyMzIgMjUuMDMwMzYsMi45MjY1IDQ0LjY2MjczLDM0LjI4NzIyIDU4LjUyNjk4LDUwLjY0MzkgMTcuMDk4NzgsMjAuMTcyNjggNjIuNzYzODYsLTEuNzE0NjcgNjYuMzA1NjYsMzIuMTM0MzMgNS4xMDI3LDQ4Ljc2NTg3IC02LjMyODQsNzguNjM3MjUgNi4xNDExLDk3LjM0MTUgMTkuOTY5MiwyOS45NTM3OSA1MC40ODY0LDE3Ljg1NTc5IDQ0LjYxOTMsODMuOTcxMTkgTSA1ODkuMTAyMjcsMzA5LjcyNzE1IGMgNC42NDM0NiwyMy43MjkyMyAxNS4wNjkwNCw3Mi43NzU3NSAxOS4wNjEyOCwxMzAuNjQyODggMC44NzIwNiwxMi42NDA0OCA1LjQ0NzE4LDI0Ljk5MjUzIDQuMjIyMzEsNDUuMjc3NTcgLTIuNTE3MjEsNDEuNjg3NSAtMTUuNzE3MDYsNDMuNjc3MjcgLTE1LjA5MTIyLDYwLjM2NDg2IDEuNDMxOTUsMzguMTgyMjQgMzAuNjEzNjEsOTMuODM3MTkgMzAuNjEzNjEsMTM5LjcwMTU0IDAsMjQuMTgwOCAtMi42Njk2NCwxMTUuMzkwNDUgNy4zMzAwMSwxMzUuMzg5NzYgMC4xNTkxMSwwLjMxODIxIDEwLjA2NDc2LDM1Ljg4MzMyIDEwLjc3OTQ1LDQ5LjE1NDI0IDAuOTQzNzgsMTcuNTI0NjkgLTI0LjQ3OCwzOS40NzAwOCAtMjguMDI2NTUsNDYuNTY3MTYgLTUuNDc3NywxMC45NTUzOSAtMzYuOTczMjQsMTAuODgxOTcgLTQwLjA5OTUsMjQuMTQ1OTUgLTMuODY4ODQsMTYuNDE0NTEgLTMuODY2Myw0My43OTczNSA0LjA0NjQ3LDU5LjQ0MTI5IG0gOTcuMzM3MzQsLTY5MS4wMDk0MSBjIC01LjAxMzMyLDM1LjUxNTk1IC00My42NTkwMSwxMS4zMTY1MiAtNTguNTM4NjEsMjMuNzgxMzEgLTIxLjMzMDE5LDE3Ljg2ODUyIC02Mi40OTk2NCwzMS40MzIxMiAtNzAuMTI0MzcsMzUuMzY3MDggLTM1LjA4NzYzLDE4LjEwNzkzIC0xMTAuNDcyMTUsLTE1LjE0MTk2IC0xMjUuNjE0MSw0LjI2ODQzIC0xNS45NTA2MywyMC40NDcwMyAtMC4wNzM1LDYxLjQ2NjQ4IC05LjE0NjY2LDg0LjE0OTI0IC02LjAzNTcsMTUuMDg5MjYgLTE4Ljg3NjcsMjMuMDE3MzQgLTI3LjQzOTk3LDMyLjkyNzk4IC0xOS43NDgyOSwyMi44NTU1NSAtNjkuOTc0MjgsNjkuODI0MTkgLTg0Ljc1OTA0LDEwMC4wMDM0NiAtNy40OTc0MSwxNS4zMDQwNCAtMy4yODQyNiw0NC40MjA0MSAtMy40NzA1Myw2My4zNDI4NCAtMC4xMjc5MywxMi45OTQxNCAtMC44MTAxNSwyMy4xMDM4NSAyLjQwMzQzLDI4LjI3NjE4IDQuOTYxNTgsNy45ODU4MSAyMy43MjA1LDI4LjExMjA3IDI0LjIzODY1LDUwLjYxMTQ5IDAuMjk0MTEsMTIuNzcxNDYgMC4wMTMzLDc4LjU5MTAxIDMuMDQ4ODgsODcuNjU1NDkgMi4zMTI1Niw2LjkwNTQ2IDQuMjIwMDQsMjYuNTY0OTcgMTAuMjEzNzcsMzYuNTg2NjIgMTEuMzU0MDEsMTguOTg0MTUgNC4zODczNyw0MC4xNTY2MiAyNy44OTczLDUzLjUwNzk1IDE5LjA1MDEyLDEwLjgxODU5IDQ2Ljg3NzgxLDEyLjIxODYyIDgxLjkyNjE4LDE0LjQ2MDU0IDMzLjcwMzQ1LDIuMTU1ODkgNjEuNTEyMTcsLTEuNDMwMzUgNzYuOTIwNzcsNi4xNDExIDExLjU4NTA4LDUuNjkyNjYgOC41ODE1MSwxNy45MzM0NCAxNC4yOTU0MSwyOS4zNjEyMyA1LjY0MDQyLDExLjI4MDg1IDMxLjUwMjYzLDExLjE1NjI3IDQxLjgwNDA5LDQzLjQ1NDg3IDcuNjA1OSwyMy44NDcxIDMuMDg1OTMsNDQuMTU2OSA2LjcwNzU1LDY1Ljg4NjYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2Nzc3NzY2Njc3Nzc3NzY2Nzc3Nzc3NjY3Nzc3Njc3NzY2Nzc3Nzc3Nzc3Nzc3Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW9wYWNpdHk6MSIKICAgICAgIGQ9Im0gNDMuMjc3ODgxLDUxNy45NDY3OSBjIDAsMCAyMzAuODQ4Mjg5LC0zLjYzODA1IDI1MC4wMDg2MzksLTMuNjU4NjcgNy40ODIyMiwtMC4wMDggOC42MTk1NCw1LjE1MTk0IDE0LjAyMDksMTEuNDU4NjkgMjQuNTk2MDgsMjguNzE4OTMgOTMuOTA5NjYsMTEyLjkzNTg1IDkzLjkwOTY2LDExMi45MzU4NSIKICAgICAgIGlkPSJwYXRoMzc4OSIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1vcGFjaXR5OjEiCiAgICAgICBkPSJtIDM1Ljk2MDU1NSw1NzcuNzA0OTQgYyAwLDAgMTY1LjUyNDU2NSwtMS42ODQ1NCAyNDguNzc5NTY1LC0xLjY4NDU0IDQuOTQ3NDksMCA3LjcyOTkzLC0yLjg4MzMgMTAuNTM3NzEsLTUuNzI5NzcgOS42NjEwNywtOS43OTQxNiAyNS42MzE5OSwtMjguNTg5OTUgMjUuNjMxOTksLTI4LjU4OTk1IgogICAgICAgaWQ9InBhdGgzNzkxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzguMzk5NjYzLDY0MS43MzE1NSA0MzEuNzA1OTMsNjM3LjQ2MzExIgogICAgICAgaWQ9InBhdGgzNzk1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOiMzMzMzNjY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gMzkuMDA5NDQyLDcwNC41Mzg1OSA1MjMuMTcyNTMsNjk3LjgzMTA0IgogICAgICAgaWQ9InBhdGgzNzk3IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzAzLjk1NzYyLDY4Mi41ODY2MSAxNDYuNzk1NDIsMS44MjkzMyBjIDEwLjUzNDAzLDAuMTMxMjcgMTQuMzQzNzQsLTIuNjM3MzkgMjUuNDg3MTUsLTYuMzcyOCAxMC40MTIxMiwtMy40OTAyNyAzMS40MjQxNSwtMi42OTg5NiA0MS4zODUzOCwtMi43NzM4NSBsIDQwNS41NjA3OSwtMy4wNDg5IgogICAgICAgaWQ9InBhdGgzNzk5IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGgzODA0IgogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDQyNi4yMTc5NCwzMTQuODkwOTggYyAyLjA2NzU0LDkuMDUyNzMgMS44NDE3Nyw1MS43Mjc3NyA2LjUwNzk0LDc0LjgzNDY2IDEuNjc0NzUsOC4yOTMzNiA4LjY3NTA4LDE0LjA2NTk4IDEwLjA1NTQxLDE0Ljg1ODYyIDQuOTAxNDcsMi44MTQ2MyAxMC44MTQ3OSw4LjE0OTgyIDEzLjA0NTc5LDE2LjA4ODMxIDYuNzU3NzksMjQuMDQ1OTEgMC44Nzk3Miw2OC40NTIxMiAwLjg3OTcyLDExMC42ODkzIDAsNi4wOTc4MiAxLjY2MDEsMzAuMTQ2NiAtMi4xNTU4OCwzMy45NjI1OSAtMi41NDA4NSwyLjU0MDgzIC0wLjI4MTYzLDEyLjk5MDY5IC0zLjQzNjc1LDE2LjE0Mzc3IGwgLTkuODQ5NDQsOS44NDMxMSBjIC0xMC4zNjcxNSwxMC4zNjA0NyAtMTEuNTkwMTcsNi41MjYxNCAtMTcuNzM4NDgsMTguODIyNzYgLTMuNTY3NzIsNy4xMzU0MyA1LjQwMjM1LDIwLjY3MjEgNy4zNTQzMiwyNC41NzYwMiAxLjkzMjE0LDMuODY0MyAtMS44NDIxNiw0Ljc3NzczIC0xLjc5MjM1LDcuNDQ2MjYgMC4yNTI4NiwxMy41NDQ4MyAyLjI5NzUsMzczLjkyNzEyIDIuMjk3NSwzNzMuOTI3MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2NS4yNDAyMiw1MTkuNzc2MTIgNC4xMTU5OSw1MDIuMTUxNTgiCiAgICAgICBpZD0icGF0aDM4MDYiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMTYuNTMxNjUsNTA0LjE4Njk5IDMuODgwNTksMzEwLjk2NDM2IgogICAgICAgaWQ9InBhdGgzODMxIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDM4ODkiCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzE3LjY3NzYsNTc2LjQ4NTM5IDEzMC4xODc0MiwxLjUyNDQ0IGMgNC41MTA3OSwzLjI0MTY5IDIwLjM0NDcxLDcuOTY4NTMgMjcuNzQ0ODYsNC4yNjg0NCAzLjE1NTQ2LC0xLjU3NzcyIDkuNDE5LC01LjM4ODE3IDE0LjAyNDg5LC0zLjk2MzU1IDQuMjY2OTgsMS4zMTk4MSA2LjAxNjg5LDMuMTE2MzIgMTAuMzY2MjEsMy4wNDg4OSAxMC4zMDQwMywtMC4xNTk3NSAyMC4yMTE3LDAuMzg3NDEgMzAuNDg4ODYsMC4zMDQ4OSAxNzcuODkwOCwtMS40MjgyNyAzNTYuNTkwMzUsLTIuMTMyNDcgNTM0Ljc3NDU2LC0zLjA0ODg4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDc1LjMwNTAxLDU4Mi44ODgwNSBjIC0zLjQ0NDE4LDExLjM1MDY2IC0yLjEwMzQzLDEyLjQzMzczIDMuNjU4NjUsMjEuMDM3MzEgMy43OTQ0NSw1LjY2NTY0IDUwLjg2MjYxLDEzLjAzODQ1IDQxLjQ2NDg1LDI3LjEzNTA5IC0xMC41MzY5NywxNS44MDU0NyAtMjIuODk3NDUsLTUuNDc3NzIgLTMzLjg0MjYzLC0xLjgyOTMzIC01LjQ1MjM2LDEuODE3NDUgLTcuMzQ5MDEsNS40NTYzMSAtMy42NTg2Niw5LjE0NjY1IDIuODA2ODMsMi44MDY4NCA0LjA0OCwxLjgwMzk2IDYuNTIwMzQsNS4xMDA0MSIKICAgICAgIGlkPSJwYXRoMzkxMCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjAxMDgyLDYzNi44NTMzMyBjIDguMzE4OTksMTMuMTEwMTYgMTguODQ2MjEsMTQuNjM0NjUgMzUuNjcxOTYsMTQuNjM0NjUgMi45Mzg2NSwwIDcuODY5OTgsLTAuOTMzNzEgMTAuNjcxMTEsMCAxMS4zNTkxNywzLjc4NjM5IDI3LjE5Mzk4LDEwLjI3NTc3IDM2LjIwMTkzLDIxLjEyOTQ4IDguMjgwMDIsOS45NzY2MSAxMC4yNTI3OCwyMy44ODMwOCA3LjcwMjAyLDM3LjEwNDI0IC02LjE2OTg5LDMxLjk3OTk4IC0xNi43MTQzMSw1Ni45ODg1MyAtMTkuMDQzNTUsODYuNTY5MDUgLTEuMzQ3OTgsMTcuMTE4OCA0LjUwOTU3LDIyLjUzNTIyIDExLjA3MTQzLDMzLjkyODU3IDEwLjY3MDIzLDE4LjUyNjcyIDguNzI0NTMsMTQuMTk5NTUgOC41NzE0MywzNC4yODU3MiAtMC4xMzk2MywxOC4zMTk0NCAwLDYwLjI2Mzg1IDAsODAuNzE0MjkiCiAgICAgICBpZD0icGF0aDM5MTIiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUyOC41MDgwNiw2NTguOTU3NzYgYyAtMTAuNjgxMjMsMC45MDQ1NCAtNy4xMDgwNCwtNS42MDI1NSAtMTAuODIzNTQsLTguMDc5NTYgLTQuNzg0NTQsLTMuMTg5NjkgLTEyLjIyNzA0LC0xLjI1MTA0IC0xNi43Njg4OCwtNS43OTI4OCAtMC42NjYxMiwtMC42NjYxMiAtOC44MDk2OSwtNC4xMDg3NyAtMTAuMTc0NDcsLTIuNzQzOTkgLTguMzY0NTksOC4zNjQ1OSAtMy4wNDg4OCwyMC41NTE4OCAtMy4wNDg4OCwzMy41Mzc3NCBsIDMuMDIyLDMzOS42OTc0MyIKICAgICAgIGlkPSJwYXRoMzkxNCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA1MTcuOTg5NDEsNjUxLjAzMDY1IGMgLTAuMjIxNzEsLTIuNzAxODQgMS45MDM0NiwtNS41NjIxMyAzLjM1Mzc3LC03LjAxMjQ1IDEuNzk5NDMsLTEuNzk5NDIgNi45MjI5NCwxLjAwNDE5IDguODQxNzgsLTAuOTE0NjYgMC4yODc2NSwtMC4yODc2NiAwLjg0MzI5LC0xMS4xNjQxIDAuMjI4NjYsLTEzLjU2NzUzIC0yLjA2NDgzLC04LjA3NDE2IC0yLjA1ODAxLC0yOC42NTY1OCAtMi4wNTgwMSwtMzguNzIwODYgbCAwLC03My4xNzMyNiIKICAgICAgIGlkPSJwYXRoMzkxNiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNTI4LjY2MDUsNjc1LjQyMTczIC0wLjQ1NzMzLC0zMS41NTU5NiIKICAgICAgIGlkPSJwYXRoMzk3NCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc2Ni4zMTYyNSw1NzkuNjQ0MzEgMC40MzExOCwxMy43OTc2OCBjIDMuMTM2NDMsNC42NjkxNSAzLjAxODI0LDkuNjAwNjggMy4wMTgyNCwxNi4zODQ3NSBsIDAsMTU3LjM3OTgxIgogICAgICAgaWQ9InBhdGgzOTgyIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMTEyMi45MDAxLDc2NS45MTMwMyBjIC0yMDIuMzA2NjksNC42OTA1IC00MDMuNzQ0MDUsLTEuMTEzODEgLTYwNS45NTQ1NCwzLjM1MzkgLTEwLjg2MzYyLDAuMjQwMDIgLTMuMzYxNDcsLTguNTg2MyAtMjguNTM2OCwtOC41ODYzIgogICAgICAgaWQ9InBhdGgzOTg0IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA4NjAuMDA4MDUsNzM3LjA2NjUxIGMgMCwwIC05Ny40NDc1LDAuODU4MDYgLTE0Ny41Njg5MiwwLjg1ODA2IC01LjI2ODYxLDAgLTQuNTE1NDYsLTguMzI5ODYgLTcuMzAwODksLTguMzI5ODYgLTMuOTc0MzUsMCAtOC42MjkyNSwwLjAyMDEgLTEwLjUwOTQ4LDAuMDM1OSAtMi4zMzQ3NywwLjAxOTcgLTEuODEwOTQsOC4zNjU5NyAtNC4xNDU4LDguMzY2OTIgLTQ2LjE2ODk5LDAuMDE4OCAtMTY3LjQwNzY3LC0xLjMwNzk5IC0xNzUuMDUyNjMsLTEuMzA3OTkgLTQuNDI5NTUsMCAtOC41NzYyNywtNi40Mzk3MiAtMTMuMTMxOTgsLTYuNDM5NzIgLTEuMzYxMTUsMCAtNi4yMzg3MywwIC0xNC4zOTQ2NywwIgogICAgICAgaWQ9InBhdGgzOTg2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3Nzc3Nzc2MiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJNIDY3NS4wMDcwMyw4MzEuMTc0MDIgNjc0LjM5NzI1LDMwOS40MDI5OSIKICAgICAgIGlkPSJwYXRoMzk4OCIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDc5OS40MDE1NywzMTMuMDYxNjUgMS4yMTk1NSw0OTUuODY2NTMiCiAgICAgICBpZD0icGF0aDM5OTAiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSA3MzYuNTk0NTIsMzEyLjQ1MTg4IC0xLjIxOTU1LDcxNi40ODgyMiIKICAgICAgIGlkPSJwYXRoMzk5MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDUzMC4wMzA5NCw2NDMuNDU4NTkgMzkyLjM3MTU5LC0zLjAxODI1IgogICAgICAgaWQ9InBhdGg0MDQ4IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gODU5LjQ1MDYsMzE0LjkwMTI4IDEuMjkzNTQsNTA3Ljk4MDU4IgogICAgICAgaWQ9InBhdGg0MDUwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjAuOTk5OTk5OTRweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gOTIxLjU0MDE3LDMxMC41ODk0OSAxLjcyNDcxLDUzMS43NTIyNyIKICAgICAgIGlkPSJwYXRoNDA1MiIKICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIC8+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDczNi4yODk2Myw0NTMuMzEwNCAxODUuNjc3MTUsLTAuMzA0ODkiCiAgICAgICBpZD0icGF0aDQxODciCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAxMDYwLjgxMDUsNTE0Ljk2NzY3IGMgMCwwIC0zNjMuMjgxMjYsLTUuNjI2MTggLTU0NC42NTA0MiwyLjUyMTc4IC00LjE3Nzc2LDAuMTg3NjkgLTEyLjUwMDQ0LDEuMDY3MTEgLTEyLjUwMDQ0LDEuMDY3MTEgLTEuNTcwOTUsMC4xMzQxIC0yLjAwMDkzLC0yLjMyNDk1IC0yLjU5MTU1LC0zLjUwNjIzIC0wLjA5NjcsLTAuMTkzNDMgLTcuMDYwODEsLTEuOTMzNCAtNy42MjIyMSwtMS4zNzE5OSAtMi44OTMxNCwyLjg5MzE0IC03LjYzMTY3LDQuMjQ4NjkgLTEyLjE5NTU1LDQuMTE2IEwgMzY5LjIwMTcsNTE0LjUzNjUiCiAgICAgICBpZD0icGF0aDQyNjEiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3NjIiAvPgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzOTkuODE1MzEsNDc5LjYxMTEyIDExLjY0MTgsNS42MDUzIGMgMi45ODQxMiwxLjQzNjc5IDYuNTI4NzgsLTAuNDc3MTIgOS45MTcwOCwtMC40MzExOCBsIDEyNy4xOTczOSwxLjcyNDcxIgogICAgICAgaWQ9InBhdGg0MjYzIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Ik0gNTE5LjI1MTUxLDUxNy4xMjM1NyA1MTguODIwMzIsMzA4LjQzMzYyIgogICAgICAgaWQ9InBhdGg0MjY1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gNDMyLjkyNTQ5LDM4OS43MTQ5OCBjIDExLjA0NDk2LDAgMzUuNTMzMDcsMC42MTkyNyA0Mi41Nzk3OCwtMS4wMDM5NyA4LjQwNTIyLC0xLjkzNjE4IDcuMDY2LC02Ljk1Mzc4IDE0LjE5NzEyLC02Ljk1Mzc4IDcuODA5NSwwIDYuNTQyOTEsOC4wNjIzNyAyMC4xNDE3LDguMDYyMzcgMTMuOTkwNjgsMCA0NC45NzY4OSwwLjM3ODg2IDYzLjkzOTkyLDAuMzc4ODYgMTIuMDgzOTUsMCA4Mi4wMDI2NiwwLjMwNDg5IDkzLjYwMDgxLDAuMzA0ODkgOC43NjA0NywwIDEzLjE1OTcsLTIuMjg4MjcgMjEuMzQyMTksLTcuMDEyNDMgNy4xOTUxNSwtNC4xNTQxMyAyLjA1NDU5LC05LjQ5MTM3IDIwLjQyNzU0LC04Ljg0MTc3IDIzLjE0NTQsMC44MTgzMyAxMi42NDMzNCwxNC4wMjQ4NyAzMi4zMTgxOSwxNC4wMjQ4NyAyNS4zNTk1NCwwIDEzMC45OTkwMiwwIDE1MC45MTk4NSwwIDE0LjMzMjQ0LDAgLTQuMTE5MTEsLTEzLjExMDIxIDI5LjI2OTMsLTEzLjQxNTEiCiAgICAgICBpZD0icGF0aDQyNjkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjc3Nzc3Nzc3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU4OC42Nzk1NyIKICAgICAgIHk9IjczNS44MDQ2MyIKICAgICAgIGlkPSJ0ZXh0NDMxMCIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMiIKICAgICAgICAgeD0iNTg4LjY3OTU3IgogICAgICAgICB5PSI3MzUuODA0NjMiPkxpbmNvbG48L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY4Ni4zOTg1IgogICAgICAgeT0iNzY1LjYyODQyIgogICAgICAgaWQ9InRleHQ0MzEwLTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNiIKICAgICAgICAgeD0iNjg2LjM5ODUiCiAgICAgICAgIHk9Ijc2NS42Mjg0MiI+SGFycnk8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgIHk9Ii04MDIuMzc3MzgiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgiCiAgICAgICAgIHg9IjcwOS44NzE4MyIKICAgICAgICAgeT0iLTgwMi4zNzczOCI+V29vZGxhd248L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjU2Mi4xMTkyNiIKICAgICAgIHk9Ii03NzEuOTY4MTQiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yIgogICAgICAgICB4PSI1NjIuMTE5MjYiCiAgICAgICAgIHk9Ii03NzEuOTY4MTQiPkVkZ2Vtb29yPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTguMzA0ODciCiAgICAgICB5PSItNzM4LjM2NjQ2IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTciCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtOC0yLTkiCiAgICAgICAgIHg9IjU5OC4zMDQ4NyIKICAgICAgICAgeT0iLTczOC4zNjY0NiI+T2xpdmVyPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICB5PSItNjc3LjIwMzk4IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00IgogICAgICAgICB4PSI1OTIuMTIyODYiCiAgICAgICAgIHk9Ii02NzcuMjAzOTgiPkhpbGxzaWRlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1OTcuMzI3MDkiCiAgICAgICB5PSItODYyLjYxNDA3IgogICAgICAgaWQ9InRleHQ0MzEwLTctMS05LTctNS0zIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDMxMi02LTgtMi05LTQtMSIKICAgICAgICAgeD0iNTk3LjMyNzA5IgogICAgICAgICB5PSItODYyLjYxNDA3Ij5Sb2NrPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1ODcuMzcwMTgiCiAgICAgICB5PSItOTI2LjEzNjYiCiAgICAgICBpZD0idGV4dDQzMTAtNy0xLTktNy01LTMtMiIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQzMTItNi04LTItOS00LTEtMyIKICAgICAgICAgeD0iNTg3LjM3MDE4IgogICAgICAgICB5PSItOTI2LjEzNjYiPldlYmI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9Ijg3MS4xNjEwMSIKICAgICAgIHk9IjYzNy41NzUyIgogICAgICAgaWQ9InRleHQ0NDY1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDY3IgogICAgICAgICB4PSI4NzEuMTYxMDEiCiAgICAgICAgIHk9IjYzNy41NzUyIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICB5PSI1NzcuMDMyNDciCiAgICAgICBpZD0idGV4dDQ0NjUtMyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00IgogICAgICAgICB4PSI4NzMuODMyMjgiCiAgICAgICAgIHk9IjU3Ny4wMzI0NyI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgaWQ9InRleHQ0NDkwIgogICAgICAgeT0iNTEwLjI2MTgxIgogICAgICAgeD0iODc1Ljk2NjQ5IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSI1MTAuMjYxODEiCiAgICAgICAgIHg9Ijg3NS45NjY0OSIKICAgICAgICAgaWQ9InRzcGFuNDQ5MiIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MjFzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iODgxLjMxNjU5IgogICAgICAgeT0iNDUwLjE5ODc2IgogICAgICAgaWQ9InRleHQ0NDk0IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NDk2IgogICAgICAgICB4PSI4ODEuMzE2NTkiCiAgICAgICAgIHk9IjQ1MC4xOTg3NiI+Mjl0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNjE1Ljc5MjQ4IgogICAgICAgeT0iMzg3Ljc0NzE2IgogICAgICAgaWQ9InRleHQ0NDY1LTMtMSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDQ2Ny00LTEiCiAgICAgICAgIHg9IjYxNS43OTI0OCIKICAgICAgICAgeT0iMzg3Ljc0NzE2Ij4zN3RoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MTkiCiAgICAgICB5PSI0ODEuNjUyODYiCiAgICAgICB4PSI0ODQuNjkwMzciCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjQ4MS42NTI4NiIKICAgICAgICAgeD0iNDg0LjY5MDM3IgogICAgICAgICBpZD0idHNwYW40NTIxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj4yNXRoPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NjMuMDQ2NzUiCiAgICAgICB5PSI1MTMuMzYxMzMiCiAgICAgICBpZD0idGV4dDQ1MjMiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1MjUiCiAgICAgICAgIHg9IjU2My4wNDY3NSIKICAgICAgICAgeT0iNTEzLjM2MTMzIj4yMXN0PC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ1MjciCiAgICAgICB5PSI1NzcuODk0ODQiCiAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTc3Ljg5NDg0IgogICAgICAgICB4PSI1NjUuOTcxNSIKICAgICAgICAgaWQ9InRzcGFuNDUyOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+MTN0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzMSIKICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICB4PSI0MzMuNTgwNzUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii00NjAuNzMzMTIiCiAgICAgICAgIHg9IjQzMy41ODA3NSIKICAgICAgICAgaWQ9InRzcGFuNDUzMyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+QW1pZG9uPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI0MDUuNTMwOTgiCiAgICAgICB5PSItNTIzLjU0MDE2IgogICAgICAgaWQ9InRleHQ0NTM1IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDUzNyIKICAgICAgICAgeD0iNDA1LjUzMDk4IgogICAgICAgICB5PSItNTIzLjU0MDE2Ij5BcmthbnNhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDUzOSIKICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICB4PSI3NDUuNDg0NjIiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0zNzIuNTg1OTQiCiAgICAgICAgIHg9Ijc0NS40ODQ2MiIKICAgICAgICAgaWQ9InRzcGFuNDU0MSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+V2VzdDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTk2LjcyODMzIgogICAgICAgeT0iLTUzMS4yNTkyOCIKICAgICAgIGlkPSJ0ZXh0NDU0MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMCwxLC0xLDAsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NDUiCiAgICAgICAgIHg9IjU5Ni43MjgzMyIKICAgICAgICAgeT0iLTUzMS4yNTkyOCI+V2FjbzwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU1NSIKICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICB4PSI1OTUuNDM0ODEiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9Ii0xMjIuNTAyOTUiCiAgICAgICAgIHg9IjU5NS40MzQ4MSIKICAgICAgICAgaWQ9InRzcGFuNDU1NyIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+TWF6aWU8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgIHk9IjE2Mi4wNjg3NyIKICAgICAgIGlkPSJ0ZXh0NDU1OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIgogICAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMC43MDcxMDY3OCwwLjcwNzEwNjc4LC0wLjcwNzEwNjc4LDAuNzA3MTA2NzgsMCwwKSI+PHRzcGFuCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjEiCiAgICAgICAgIHg9IjY5NS43NzI5NSIKICAgICAgICAgeT0iMTYyLjA2ODc3Ij5ab288L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjI0MC41ODk5NyIKICAgICAgIHk9IjU3NC40NDU0MyIKICAgICAgIGlkPSJ0ZXh0NDU2MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU2NSIKICAgICAgICAgeD0iMjQwLjU4OTk3IgogICAgICAgICB5PSI1NzQuNDQ1NDMiPjEzdGg8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU2NyIKICAgICAgIHk9IjUxMS42MzY2MyIKICAgICAgIHg9IjIwNi4wMzE3NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNTExLjYzNjYzIgogICAgICAgICB4PSIyMDYuMDMxNzUiCiAgICAgICAgIGlkPSJ0c3BhbjQ1NjkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPjIxc3Q8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjYyMC40NDMxMiIKICAgICAgIHk9Ii01MDYuNjgyMTkiCiAgICAgICBpZD0idGV4dDQ1NzEiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40NTczIgogICAgICAgICB4PSI2MjAuNDQzMTIiCiAgICAgICAgIHk9Ii01MDYuNjgyMTkiPk5pbXM8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDU4MyIKICAgICAgIHk9IjY5OC44NDAwOSIKICAgICAgIHg9IjM3MC4yMTY4NiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iNjk4Ljg0MDA5IgogICAgICAgICB4PSIzNzAuMjE2ODYiCiAgICAgICAgIGlkPSJ0c3BhbjQ1ODUiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1hcGxlPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSIzODQuMDg0MiIKICAgICAgIHk9IjY4MC44NTEzOCIKICAgICAgIGlkPSJ0ZXh0NDU5OSIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDYwMSIKICAgICAgICAgeD0iMzg0LjA4NDIiCiAgICAgICAgIHk9IjY4MC44NTEzOCI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0ibSAzNjcuOTA4MTcsMTAwOS45NTk2IDI2My4wMTgzMywwIgogICAgICAgaWQ9InBhdGg0NjA1IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgLz4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDciCiAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgeD0iNzM2LjI2NzQ2IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3BhbgogICAgICAgICB5PSItNDMzLjEzNzc2IgogICAgICAgICB4PSI3MzYuMjY3NDYiCiAgICAgICAgIGlkPSJ0c3BhbjQ2MDkiCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1lcmlkaWFuPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ5NzkiCiAgICAgICB5PSI2NDAuMjA1MjYiCiAgICAgICB4PSI1NzIuODMyMTUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuCiAgICAgICAgIHk9IjY0MC4yMDUyNiIKICAgICAgICAgeD0iNTcyLjgzMjE1IgogICAgICAgICBpZD0idHNwYW40OTgxIgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIj5DZW50cmFsPC90c3Bhbj48L3RleHQ+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI1NzUuMDg5NjYiCiAgICAgICB5PSI2NzAuOTAzNSIKICAgICAgIGlkPSJ0ZXh0NDk4MyIKICAgICAgIHNvZGlwb2RpOmxpbmVzcGFjaW5nPSIxMjUlIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDk4NSIKICAgICAgICAgeD0iNTc1LjA4OTY2IgogICAgICAgICB5PSI2NzAuOTAzNSI+RG91Z2xhczwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNDk5LjQ4OTYyIgogICAgICAgeT0iMTAwOC42MDY5IgogICAgICAgaWQ9InRleHQ1MDQ3IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5IgogICAgICAgICB4PSI0OTkuNDg5NjIiCiAgICAgICAgIHk9IjEwMDguNjA2OSI+NDd0aDwvdHNwYW4+PC90ZXh0PgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iMjE2LjY0NTQzIgogICAgICAgeT0iNzI1Ljk4Mjk3IgogICAgICAgaWQ9InRleHQ1MDUxIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDUzIgogICAgICAgICB4PSIyMTYuNjQ1NDMiCiAgICAgICAgIHk9IjcyNS45ODI5NyI+S2VsbG9nZzwvdHNwYW4+PC90ZXh0PgogICAgPGZsb3dSb290CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgaWQ9ImZsb3dSb290NTA1NSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MThweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIj48Zmxvd1JlZ2lvbgogICAgICAgICBpZD0iZmxvd1JlZ2lvbjUwNTciPjxyZWN0CiAgICAgICAgICAgaWQ9InJlY3Q1MDU5IgogICAgICAgICAgIHdpZHRoPSIzNDMuNTcxNDQiCiAgICAgICAgICAgaGVpZ2h0PSIxMDMuNTcxNDMiCiAgICAgICAgICAgeD0iMTkuMjg1NzE1IgogICAgICAgICAgIHk9IjE3LjE0Mjg1NyIKICAgICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIgLz48L2Zsb3dSZWdpb24+PGZsb3dQYXJhCiAgICAgICAgIGlkPSJmbG93UGFyYTUwNjEiPjwvZmxvd1BhcmE+PC9mbG93Um9vdD4gICAgPHRleHQKICAgICAgIHRyYW5zZm9ybT0ibWF0cml4KDAsMSwtMSwwLDAsMCkiCiAgICAgICBzb2RpcG9kaTpsaW5lc3BhY2luZz0iMTI1JSIKICAgICAgIGlkPSJ0ZXh0NDYwNy03IgogICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgIHg9Ijc3NC44NzU2MSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTUwOC4xODk3MyIKICAgICAgICAgeD0iNzc0Ljg3NTYxIgogICAgICAgICBpZD0idHNwYW40NjA5LTciCiAgICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiPk1jQ2xlYW48L3RzcGFuPjwvdGV4dD4KICAgIDxwYXRoCiAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtmaWxsOm5vbmU7c3Ryb2tlOiMzMzMzNjY7c3Ryb2tlLXdpZHRoOjFweDtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIKICAgICAgIGQ9Im0gMzY0LjE1OTk5LDY1OC40Mjg5MSAyOTkuNTEwMjMsLTEuMDEwMTYgYyA2LjQ5ODcyLC0wLjAyMTkgNi45NzcxOSw5LjI1NDEyIDE2LjU5NjMxLDkuMzkyNDcgMTIuMDU0MjcsMC4xNzMzOSAyOS4xMTA4MywtMC41MzU3MiA1NC4xMTQzNywtMC4zMDExIgogICAgICAgaWQ9InBhdGg1NDQwIgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzYyIgLz4KICAgIDx0ZXh0CiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZTo5LjY1ODM3NzY1cHg7Zm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtsaW5lLWhlaWdodDoxMjUlO2xldHRlci1zcGFjaW5nOjBweDt3b3JkLXNwYWNpbmc6MHB4O2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtmb250LWZhbWlseTpWZXJkYW5hOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246VmVyZGFuYSIKICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgIHk9Ijk0NC4zNTc1NCIKICAgICAgIGlkPSJ0ZXh0NTA0Ny05IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW41MDQ5LTMiCiAgICAgICAgIHg9IjM3My45OTMwNCIKICAgICAgICAgeT0iOTQ0LjM1NzU0Ij5NYWNBcnRodXI8L3RzcGFuPjwvdGV4dD4KICAgIDx0ZXh0CiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICBpZD0idGV4dDQ2MDctNy0xIgogICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgIHg9Ijc4MC44NDYwNyIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4KICAgICAgICAgeT0iLTQ5MC4yNDU5NyIKICAgICAgICAgeD0iNzgwLjg0NjA3IgogICAgICAgICBpZD0idHNwYW40NjA5LTctOSIKICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSI+U2VuZWNhPC90c3Bhbj48L3RleHQ+CiAgICA8cGF0aAogICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7ZmlsbDpub25lO3N0cm9rZTojMzMzMzY2O3N0cm9rZS13aWR0aDoxcHg7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiCiAgICAgICBkPSJtIDM2Ny42OTU1Myw1MzcuMjEwNiAxNDEuMjgzMDMsLTEuMDEwMTUgYyA2LjQ4OTk5LC0wLjA0NjQgMTIuNzgxMTQsNy4yMzU0NSAxOS4xOTI5LDcuMzIzNiA1NS45MjM2MiwwLjc2ODkgMTU4LjY4OTk3LC0wLjE3MzMzIDIzNi41MTQwMiwtMS4wMTAxNSA3LjgzOTU2LC0wLjA4NDMgMjIuNjMxNDcsLTE5Ljg1MzU1IDMwLjMwNDU3LC0yMC40NTU1OSAyMi4yNjU4OSwtMS4zNTE4MSA0NS4xNzk0NSwtMC41MDUwNyA2Ny42ODAyMiwtMC41MDUwNyAxNi4xNDczMSwtMC42MzI0MSAzLjYxMDE2LDIwLjcwODEzIDI2Ljc2OTA0LDIwLjcwODEzIGwgMjQzLjQ0Njc5LC0xLjAxMDE2IgogICAgICAgaWQ9InBhdGg1NDk2IgogICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjg3LjM2MjE4KSIKICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY3NzY2NjY2MiIC8+CiAgICA8dGV4dAogICAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICAgIHN0eWxlPSJmb250LXNpemU6OS42NTgzNzc2NXB4O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7bGluZS1oZWlnaHQ6MTI1JTtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7Zm9udC1mYW1pbHk6VmVyZGFuYTstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOlZlcmRhbmEiCiAgICAgICB4PSI2ODUuMjA4MTMiCiAgICAgICB5PSI4MjcuNTMwODIiCiAgICAgICBpZD0idGV4dDQzMTAtNy04IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW40MzEyLTYtNiIKICAgICAgICAgeD0iNjg1LjIwODEzIgogICAgICAgICB5PSI4MjcuNTMwODIiPlBhd25lZTwvdHNwYW4+PC90ZXh0PgogICAgPHBhdGgKICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzMzMzM2NjtzdHJva2Utd2lkdGg6MXB4O3N0cm9rZS1saW5lY2FwOmJ1dHQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTtlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIgogICAgICAgZD0iTSA1NTQuMjg1NzIsNzIxLjQyODU3IDU1MCw1NDMuMjE0MjkgNTQ3LjE0Mjg2LDEwMi41IDU0Ni43ODU3MiwyMy4yMTQyODUiCiAgICAgICBpZD0icGF0aDU1MTkiCiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwyODcuMzYyMTgpIiAvPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjkuNjU4Mzc3NjVweDtmb250LXN0eWxlOm5vcm1hbDtmb250LXZhcmlhbnQ6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXN0cmV0Y2g6bm9ybWFsO2xpbmUtaGVpZ2h0OjEyNSU7bGV0dGVyLXNwYWNpbmc6MHB4O3dvcmQtc3BhY2luZzowcHg7ZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lO2ZvbnQtZmFtaWx5OlZlcmRhbmE7LWlua3NjYXBlLWZvbnQtc3BlY2lmaWNhdGlvbjpWZXJkYW5hIgogICAgICAgeD0iNTI5LjYyNTMxIgogICAgICAgeT0iLTU1MC44NDc3OCIKICAgICAgIGlkPSJ0ZXh0NDU0My01IgogICAgICAgc29kaXBvZGk6bGluZXNwYWNpbmc9IjEyNSUiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCgwLDEsLTEsMCwwLDApIj48dHNwYW4KICAgICAgICAgc29kaXBvZGk6cm9sZT0ibGluZSIKICAgICAgICAgaWQ9InRzcGFuNDU0NS0wIgogICAgICAgICB4PSI1MjkuNjI1MzEiCiAgICAgICAgIHk9Ii01NTAuODQ3NzgiPkJyb2Fkd2F5PC90c3Bhbj48L3RleHQ+CiAgPC9nPgo8L3N2Zz4K\",\"xPosKeyName\":\"xPos\",\"yPosKeyName\":\"yPos\",\"posFunction\":\"return {x: origXPos, y: origYPos};\",\"markerOffsetX\":0.5,\"markerOffsetY\":1,\"showTooltip\":true,\"autocloseTooltip\":true,\"showTooltipAction\":\"click\",\"defaultCenterPosition\":[0,0]},\"title\":\"Markers Placement - Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"c39f512a-21c6-6b06-3aa1-715262c6553d\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}"
+ }
+ },
+ {
+ "alias": "markers_placement_openstreetmap",
+ "name": "Markers Placement - OpenStreetMap",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 8.5,
+ "sizeY": 6,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": ".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane { z-index: 4; }\n\n.leaflet-tile-pane { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane { z-index: 5; }\n.leaflet-marker-pane { z-index: 6; }\n.leaflet-tooltip-pane { z-index: 7; }\n.leaflet-popup-pane { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.tb-marker-label {\n border: none;\n background: none;\n box-shadow: none;\n}\n\n.tb-marker-label:before {\n border: none;\n background: none;\n}\n",
+ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('openstreet-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.7867521952070078,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.7040053227577256,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName} Latitude: ${latitude:7}Longitude: ${longitude:7}Delete \",\"markerImageSize\":34,\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":[0,0],\"customProviderTileUrl\":\"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"defaultZoomLevel\":5},\"title\":\"Markers Placement - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"54c293c4-9ca6-e34f-dc6a-0271944c1c66\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}"
+ }
+ },
+ {
+ "alias": "markers_placement_google_maps",
+ "name": "Markers Placement - Google Maps",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 8.5,
+ "sizeY": 6,
+ "resources": [],
+ "templateHtml": "",
+ "templateCss": ".error {\n color: red;\n}\n.tb-labels {\n color: #222;\n font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n text-align: center;\n width: 200px;\n white-space: nowrap;\n}",
+ "controllerScript": "self.onInit = function() {\n self.ctx.map = new TbMapWidgetV2('google-map', false, self.ctx, null, null, true);\n var createEntityLocation = {\n name: 'action.add',\n show: true,\n onAction: function($event) {\n self.ctx.map.selectEntity($event);\n },\n icon: 'add_location'\n };\n self.ctx.widgetActions = [createEntityLocation];\n}\n\nself.onDataUpdated = function() {\n self.ctx.map.update();\n}\n\nself.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"First point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"Second point\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName} Latitude: ${latitude:7}Longitude: ${longitude:7}Delete \",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":false,\"markerImages\":[],\"useMarkerImageFunction\":false,\"colorFunction\":\"\\n\",\"color\":\"#fe7569\",\"showTooltip\":true,\"autocloseTooltip\":true,\"defaultCenterPosition\":[0,0],\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"zoomOnClick\":true,\"gridSize\":60,\"defaultZoomLevel\":5},\"title\":\"Markers Placement - Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"tooltipAction\":[{\"id\":\"8d3c0156-0a14-7a6f-0ddd-0ec16b9ffc91\",\"name\":\"delete\",\"icon\":\"more_horiz\",\"type\":\"custom\",\"customFunction\":\"var $rootScope = widgetContext.$scope.$injector.get('$rootScope');\\nvar entityDatasource = widgetContext.map.subscription.datasources.filter(\\n function(entity) {\\n return entity.entityId === entityId.id\\n });\\n\\nwidgetContext.map.saveMarkerLocation(entityDatasource[0],\\n widgetContext.map.locations[0], {\\n \\\"lat\\\": null,\\n \\\"lng\\\": null\\n }).then(function succes() {\\n $rootScope.$broadcast('widgetForceReInit');\\n });\"}]},\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"displayTimewindow\":true}"
+ }
}
]
}
diff --git a/application/src/main/data/upgrade/2.4.2/schema_update.sql b/application/src/main/data/upgrade/2.4.2/schema_update.sql
new file mode 100644
index 0000000000..79bbc91fec
--- /dev/null
+++ b/application/src/main/data/upgrade/2.4.2/schema_update.sql
@@ -0,0 +1,31 @@
+--
+-- Copyright © 2016-2019 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.
+--
+
+DROP INDEX IF EXISTS idx_alarm_originator_alarm_type;
+
+CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(originator_id, type, start_ts DESC);
+
+CREATE INDEX IF NOT EXISTS idx_device_customer_id ON device(tenant_id, customer_id);
+
+CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id, customer_id, type);
+
+CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type);
+
+CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id);
+
+CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type);
+
+CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type);
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 42b66a73e8..f15a8bff63 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -32,6 +32,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleChainTransactionService;
@@ -88,6 +90,7 @@ import java.io.StringWriter;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Component
@@ -285,6 +288,23 @@ public class ActorSystemContext {
@Getter
private long statisticsPersistFrequency;
+ @Getter
+ private final AtomicInteger jsInvokeRequestsCount = new AtomicInteger(0);
+ @Getter
+ private final AtomicInteger jsInvokeResponsesCount = new AtomicInteger(0);
+ @Getter
+ private final AtomicInteger jsInvokeFailuresCount = new AtomicInteger(0);
+
+ @Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}")
+ public void printStats() {
+ if (statisticsEnabled) {
+ if (jsInvokeRequestsCount.get() > 0 || jsInvokeResponsesCount.get() > 0 || jsInvokeFailuresCount.get() > 0) {
+ log.info("Rule Engine JS Invoke Stats: requests [{}] responses [{}] failures [{}]",
+ jsInvokeRequestsCount.getAndSet(0), jsInvokeResponsesCount.getAndSet(0), jsInvokeFailuresCount.getAndSet(0));
+ }
+ }
+ }
+
@Value("${actors.tenant.create_components_on_init}")
@Getter
private boolean tenantComponentsInitEnabled;
@@ -336,6 +356,10 @@ public class ActorSystemContext {
@Getter
private CassandraBufferedRateExecutor cassandraBufferedRateExecutor;
+ @Autowired(required = false)
+ @Getter
+ private RedisTemplate redisTemplate;
+
public ActorSystemContext() {
config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
}
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 aa1f8bc909..6784a2a0f3 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
@@ -223,6 +223,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
void process(ActorContext context, TransportToDeviceActorMsgWrapper wrapper) {
+ boolean reportDeviceActivity = false;
TransportToDeviceActorMsg msg = wrapper.getMsg();
if (msg.hasSessionEvent()) {
processSessionStateMsgs(msg.getSessionInfo(), msg.getSessionEvent());
@@ -235,11 +236,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
if (msg.hasPostAttributes()) {
handlePostAttributesRequest(context, msg.getSessionInfo(), msg.getPostAttributes());
- reportLogicalDeviceActivity();
+ reportDeviceActivity = true;
}
if (msg.hasPostTelemetry()) {
handlePostTelemetryRequest(context, msg.getSessionInfo(), msg.getPostTelemetry());
- reportLogicalDeviceActivity();
+ reportDeviceActivity = true;
}
if (msg.hasGetAttributes()) {
handleGetAttributesRequest(context, msg.getSessionInfo(), msg.getGetAttributes());
@@ -249,11 +250,14 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
}
if (msg.hasToServerRPCCallRequest()) {
handleClientSideRPCRequest(context, msg.getSessionInfo(), msg.getToServerRPCCallRequest());
- reportLogicalDeviceActivity();
+ reportDeviceActivity = true;
}
if (msg.hasSubscriptionInfo()) {
handleSessionActivity(context, msg.getSessionInfo(), msg.getSubscriptionInfo());
}
+ if (reportDeviceActivity) {
+ reportLogicalDeviceActivity();
+ }
}
private void reportLogicalDeviceActivity() {
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 5cfc2f9a19..e5e326e597 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
@@ -22,8 +22,9 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.channel.EventLoopGroup;
+import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
-import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleChainTransactionService;
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest;
@@ -60,7 +61,6 @@ import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
-import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
@@ -229,6 +229,27 @@ class DefaultTbContext implements TbContext {
return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), nodeCtx.getSelf().getId(), script, argNames);
}
+ @Override
+ public void logJsEvalRequest() {
+ if (mainCtx.isStatisticsEnabled()) {
+ mainCtx.getJsInvokeRequestsCount().incrementAndGet();
+ }
+ }
+
+ @Override
+ public void logJsEvalResponse() {
+ if (mainCtx.isStatisticsEnabled()) {
+ mainCtx.getJsInvokeResponsesCount().incrementAndGet();
+ }
+ }
+
+ @Override
+ public void logJsEvalFailure() {
+ if (mainCtx.isStatisticsEnabled()) {
+ mainCtx.getJsInvokeFailuresCount().incrementAndGet();
+ }
+ }
+
@Override
public String getNodeId() {
return mainCtx.getNodeIdProvider().getNodeId();
@@ -361,6 +382,16 @@ class DefaultTbContext implements TbContext {
return mainCtx.getCassandraBufferedRateExecutor().submit(task);
}
+ @Override
+ public RedisTemplate getRedisTemplate() {
+ return mainCtx.getRedisTemplate();
+ }
+
+ @Override
+ public String getServerAddress() {
+ return mainCtx.getServerAddress();
+ }
+
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index bc28042fb2..852fbe2884 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -22,8 +22,10 @@ import akka.actor.Terminated;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
@@ -50,6 +52,7 @@ import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.discovery.ServerInstance;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.state.DeviceStateService;
+import org.thingsboard.server.service.transport.RuleEngineStats;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.duration.Duration;
@@ -58,6 +61,7 @@ import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.gen.cluster.ClusterAPIProtos.MessageType.CLUSTER_ACTOR_MESSAGE;
@@ -187,8 +191,29 @@ public class DefaultActorService implements ActorService {
this.rpcManagerActor.tell(msg, ActorRef.noSender());
}
+ @Value("${cluster.stats.enabled:false}")
+ private boolean statsEnabled;
+
+ private final AtomicInteger sentClusterMsgs = new AtomicInteger(0);
+ private final AtomicInteger receivedClusterMsgs = new AtomicInteger(0);
+
+
+ @Scheduled(fixedDelayString = "${cluster.stats.print_interval_ms}")
+ public void printStats() {
+ if (statsEnabled) {
+ int sent = sentClusterMsgs.getAndSet(0);
+ int received = receivedClusterMsgs.getAndSet(0);
+ if (sent > 0 || received > 0) {
+ log.info("Cluster msgs sent [{}] received [{}]", sent, received);
+ }
+ }
+ }
+
@Override
public void onReceivedMsg(ServerAddress source, ClusterAPIProtos.ClusterMessage msg) {
+ if (statsEnabled) {
+ receivedClusterMsgs.incrementAndGet();
+ }
ServerAddress serverAddress = new ServerAddress(source.getHost(), source.getPort(), source.getServerType());
if (log.isDebugEnabled()) {
log.info("Received msg [{}] from [{}]", msg.getMessageType().name(), serverAddress);
@@ -239,11 +264,17 @@ public class DefaultActorService implements ActorService {
@Override
public void onSendMsg(ClusterAPIProtos.ClusterMessage msg) {
+ if (statsEnabled) {
+ sentClusterMsgs.incrementAndGet();
+ }
rpcManagerActor.tell(msg, ActorRef.noSender());
}
@Override
public void onRpcSessionCreateRequestMsg(RpcSessionCreateRequestMsg msg) {
+ if (statsEnabled) {
+ sentClusterMsgs.incrementAndGet();
+ }
rpcManagerActor.tell(msg, ActorRef.noSender());
}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
index fc25af3a56..56c0bc1974 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
@@ -84,7 +84,7 @@ public class ThingsboardMessageConfiguration {
}
@Slf4j
- static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader {
+ public static class SpringResourceLoader extends org.apache.velocity.runtime.resource.loader.ResourceLoader {
public static final String NAME = "spring";
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index a6122dd82e..81415ae35d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -28,12 +28,12 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.service.security.model.SecuritySettings;
+import org.thingsboard.server.common.data.security.model.SecuritySettings;
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.service.update.UpdateService;
-import org.thingsboard.server.service.update.model.UpdateMessage;
+import org.thingsboard.server.common.data.UpdateMessage;
@RestController
@RequestMapping("/api/admin")
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 690ba76a0f..f744f5d822 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -24,7 +24,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -43,14 +42,12 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
-import org.thingsboard.server.service.security.model.SecuritySettings;
+import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.service.security.model.SecurityUser;
-import org.thingsboard.server.service.security.model.UserPasswordPolicy;
+import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtToken;
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 ua_parser.Client;
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 3fc809c424..77a9d3ad0f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -516,15 +516,27 @@ public abstract class BaseController {
protected String constructBaseUrl(HttpServletRequest request) {
String scheme = request.getScheme();
- if (request.getHeader("x-forwarded-proto") != null) {
- scheme = request.getHeader("x-forwarded-proto");
+
+ String forwardedProto = request.getHeader("x-forwarded-proto");
+ if (forwardedProto != null) {
+ scheme = forwardedProto;
}
+
int serverPort = request.getServerPort();
if (request.getHeader("x-forwarded-port") != null) {
try {
serverPort = request.getIntHeader("x-forwarded-port");
} catch (NumberFormatException e) {
}
+ } else if (forwardedProto != null) {
+ switch (forwardedProto) {
+ case "http":
+ serverPort = 80;
+ break;
+ case "https":
+ serverPort = 443;
+ break;
+ }
}
String baseUrl = String.format("%s://%s:%d",
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 f497c4fefa..698dcfb3f3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
-import org.thingsboard.server.controller.claim.data.ClaimRequest;
+import org.thingsboard.server.common.data.ClaimRequest;
import org.thingsboard.server.dao.device.claim.ClaimResponse;
import org.thingsboard.server.dao.device.claim.ClaimResult;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
index 8d08179409..1812a4f90f 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -24,7 +24,6 @@ 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.Event;
-import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@@ -32,9 +31,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TimePageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.event.EventService;
-import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.security.permission.Operation;
-import org.thingsboard.server.service.security.permission.Resource;
@RestController
@RequestMapping("/api")
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 f384817465..56c90a2dab 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -119,6 +119,11 @@ public class ThingsboardInstallService {
case "2.4.0":
log.info("Upgrading ThingsBoard from version 2.4.0 to 2.4.1 ...");
+ case "2.4.1":
+ log.info("Upgrading ThingsBoard from version 2.4.1 to 2.4.2 ...");
+
+ databaseUpgradeService.upgradeDatabase("2.4.1");
+
log.info("Updating system data...");
systemDataLoaderService.deleteSystemWidgetBundle("charts");
@@ -133,6 +138,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.deleteSystemWidgetBundle("gateway_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("input_widgets");
systemDataLoaderService.deleteSystemWidgetBundle("date");
+ systemDataLoaderService.deleteSystemWidgetBundle("entity_admin_widgets");
systemDataLoaderService.loadSystemWidgets();
break;
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
index 425b578f7b..dabca5f3d7 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
@@ -30,13 +30,6 @@ public interface ClusterRoutingService extends DiscoveryServiceListener {
ServerAddress getCurrentServer();
- Optional resolveByUuid(UUID uuid);
-
Optional resolveById(EntityId entityId);
- Optional resolveByUuid(ServerType server, UUID uuid);
-
- Optional resolveById(ServerType server, EntityId entityId);
-
-
}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
index 87948eddba..99051389c8 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
@@ -88,21 +88,6 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService {
return resolveByUuid(rootCircle, entityId.getId());
}
- @Override
- public Optional resolveByUuid(UUID uuid) {
- return resolveByUuid(rootCircle, uuid);
- }
-
- @Override
- public Optional resolveByUuid(ServerType server, UUID uuid) {
- return resolveByUuid(circles[server.ordinal()], uuid);
- }
-
- @Override
- public Optional resolveById(ServerType server, EntityId entityId) {
- return resolveByUuid(circles[server.ordinal()], entityId.getId());
- }
-
private Optional resolveByUuid(ConsistentHashCircle circle, UUID uuid) {
Assert.notNull(uuid);
if (circle.isEmpty()) {
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java
index 82604b63a3..1a4c654ea3 100644
--- a/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java
+++ b/application/src/main/java/org/thingsboard/server/service/executors/ClusterRpcCallbackExecutorService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class ClusterRpcCallbackExecutorService extends AbstractListeningExecutor {
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
index 40b4302d16..2c8678fd45 100644
--- a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
+++ b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class DbCallbackExecutorService extends AbstractListeningExecutor {
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java
index 200cfef090..f61e0bd450 100644
--- a/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java
+++ b/application/src/main/java/org/thingsboard/server/service/executors/ExternalCallExecutorService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.executors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class ExternalCallExecutorService extends AbstractListeningExecutor {
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
index c73c12e65f..68ca728823 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java
@@ -267,6 +267,15 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService {
} catch (InvalidQueryException e) {}
log.info("Schema updated.");
break;
+ case "2.4.1":
+ log.info("Updating schema ...");
+ String updateAssetTableStmt = "alter table asset add label text";
+ try {
+ cluster.getSession().execute(updateAssetTableStmt);
+ Thread.sleep(2500);
+ } catch (InvalidQueryException e) {}
+ log.info("Schema updated.");
+ 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/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index d086e4be8b..3210742388 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
@@ -176,6 +176,26 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService {
log.info("Schema updated.");
}
break;
+ case "2.4.1":
+ try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+ log.info("Updating schema ...");
+ try {
+ conn.createStatement().execute("ALTER TABLE asset ADD COLUMN label varchar(255)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+ } catch (Exception e) {}
+ schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "2.4.2", SCHEMA_UPDATE_SQL);
+ loadSql(schemaUpdateFile, conn);
+ try {
+ conn.createStatement().execute("ALTER TABLE device ADD CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+ } catch (Exception e) {}
+ try {
+ conn.createStatement().execute("ALTER TABLE device_credentials ADD CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+ } catch (Exception e) {}
+ try {
+ conn.createStatement().execute("ALTER TABLE asset ADD CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name)"); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script
+ } catch (Exception 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/mail/MailExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
index b56d884331..9a114dbba5 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
@@ -17,7 +17,7 @@ package org.thingsboard.server.service.mail;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
-import org.thingsboard.server.service.executors.AbstractListeningExecutor;
+import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class MailExecutorService extends AbstractListeningExecutor {
diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
index 1252448b16..334e1b8772 100644
--- a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
+++ b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
@@ -17,7 +17,7 @@ package org.thingsboard.server.service.script;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
-import org.thingsboard.server.service.executors.AbstractListeningExecutor;
+import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class JsExecutorService extends AbstractListeningExecutor {
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
index 4f73a59961..00c50940c4 100644
--- a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.script;
+import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Getter;
@@ -22,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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.kafka.TBKafkaConsumerTemplate;
@@ -29,13 +31,16 @@ import org.thingsboard.server.kafka.TBKafkaProducerTemplate;
import org.thingsboard.server.kafka.TbKafkaRequestTemplate;
import org.thingsboard.server.kafka.TbKafkaSettings;
import org.thingsboard.server.kafka.TbNodeIdProvider;
-import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
@Slf4j
@ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "remote", matchIfMissing = true)
@@ -70,6 +75,30 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
@Value("${js.remote.max_errors}")
private int maxErrors;
+ @Value("${js.remote.stats.enabled:false}")
+ private boolean statsEnabled;
+
+ private final AtomicInteger kafkaPushedMsgs = new AtomicInteger(0);
+ private final AtomicInteger kafkaInvokeMsgs = new AtomicInteger(0);
+ private final AtomicInteger kafkaEvalMsgs = new AtomicInteger(0);
+ private final AtomicInteger kafkaFailedMsgs = new AtomicInteger(0);
+ private final AtomicInteger kafkaTimeoutMsgs = new AtomicInteger(0);
+
+ @Scheduled(fixedDelayString = "${js.remote.stats.print_interval_ms}")
+ public void printStats() {
+ if (statsEnabled) {
+ int pushedMsgs = kafkaPushedMsgs.getAndSet(0);
+ int invokeMsgs = kafkaInvokeMsgs.getAndSet(0);
+ int evalMsgs = kafkaEvalMsgs.getAndSet(0);
+ int failed = kafkaFailedMsgs.getAndSet(0);
+ int timedOut = kafkaTimeoutMsgs.getAndSet(0);
+ if (pushedMsgs > 0 || invokeMsgs > 0 || evalMsgs > 0 || failed > 0 || timedOut > 0) {
+ log.info("Kafka JS Invoke Stats: pushed [{}] received [{}] invoke [{}] eval [{}] failed [{}] timedOut [{}]",
+ pushedMsgs, invokeMsgs + evalMsgs, invokeMsgs, evalMsgs, failed, timedOut);
+ }
+ }
+ }
+
private TbKafkaRequestTemplate kafkaTemplate;
private Map scriptIdToBodysMap = new ConcurrentHashMap<>();
@@ -80,22 +109,6 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
requestBuilder.clientId("producer-js-invoke-" + nodeIdProvider.getNodeId());
requestBuilder.defaultTopic(requestTopic);
requestBuilder.encoder(new RemoteJsRequestEncoder());
- requestBuilder.enricher((request, responseTopic, requestId) -> {
- JsInvokeProtos.RemoteJsRequest.Builder remoteRequest = JsInvokeProtos.RemoteJsRequest.newBuilder();
- if (request.hasCompileRequest()) {
- remoteRequest.setCompileRequest(request.getCompileRequest());
- }
- if (request.hasInvokeRequest()) {
- remoteRequest.setInvokeRequest(request.getInvokeRequest());
- }
- if (request.hasReleaseRequest()) {
- remoteRequest.setReleaseRequest(request.getReleaseRequest());
- }
- remoteRequest.setResponseTopic(responseTopic);
- remoteRequest.setRequestIdMSB(requestId.getMostSignificantBits());
- remoteRequest.setRequestIdLSB(requestId.getLeastSignificantBits());
- return remoteRequest.build();
- });
TBKafkaConsumerTemplate.TBKafkaConsumerTemplateBuilder responseBuilder = TBKafkaConsumerTemplate.builder();
responseBuilder.settings(kafkaSettings);
@@ -138,7 +151,22 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.build();
log.trace("Post compile request for scriptId [{}]", scriptId);
- ListenableFuture future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper);
+ ListenableFuture future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper);
+ kafkaPushedMsgs.incrementAndGet();
+ Futures.addCallback(future, new FutureCallback() {
+ @Override
+ public void onSuccess(@Nullable JsInvokeProtos.RemoteJsResponse result) {
+ kafkaEvalMsgs.incrementAndGet();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
+ kafkaTimeoutMsgs.incrementAndGet();
+ }
+ kafkaFailedMsgs.incrementAndGet();
+ }
+ });
return Futures.transform(future, response -> {
JsInvokeProtos.JsCompileResponse compilationResult = response.getCompileResponse();
UUID compiledScriptId = new UUID(compilationResult.getScriptIdMSB(), compilationResult.getScriptIdLSB());
@@ -174,7 +202,22 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setInvokeRequest(jsRequestBuilder.build())
.build();
- ListenableFuture future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper);
+ ListenableFuture future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper);
+ kafkaPushedMsgs.incrementAndGet();
+ Futures.addCallback(future, new FutureCallback() {
+ @Override
+ public void onSuccess(@Nullable JsInvokeProtos.RemoteJsResponse result) {
+ kafkaInvokeMsgs.incrementAndGet();
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
+ kafkaTimeoutMsgs.incrementAndGet();
+ }
+ kafkaFailedMsgs.incrementAndGet();
+ }
+ });
return Futures.transform(future, response -> {
JsInvokeProtos.JsInvokeResponse invokeResult = response.getInvokeResponse();
if (invokeResult.getSuccess()) {
@@ -197,7 +240,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setReleaseRequest(jsRequest)
.build();
- ListenableFuture future = kafkaTemplate.post(scriptId.toString(), jsRequestWrapper);
+ ListenableFuture future = kafkaTemplate.post(UUID.randomUUID().toString(), jsRequestWrapper);
JsInvokeProtos.RemoteJsResponse response = future.get();
JsInvokeProtos.JsReleaseResponse compilationResult = response.getReleaseResponse();
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java
index db1a75add5..d42d221d24 100644
--- a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java
+++ b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java
@@ -15,15 +15,23 @@
*/
package org.thingsboard.server.service.script;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.kafka.TbKafkaEncoder;
+import java.nio.charset.StandardCharsets;
+
/**
* Created by ashvayka on 25.09.18.
*/
public class RemoteJsRequestEncoder implements TbKafkaEncoder {
@Override
public byte[] encode(JsInvokeProtos.RemoteJsRequest value) {
- return value.toByteArray();
+ try {
+ return JsonFormat.printer().print(value).getBytes(StandardCharsets.UTF_8);
+ } catch (InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java
index 0ac7e6f647..8407ceaaa1 100644
--- a/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java
+++ b/application/src/main/java/org/thingsboard/server/service/script/RemoteJsResponseDecoder.java
@@ -15,10 +15,12 @@
*/
package org.thingsboard.server.service.script;
+import com.google.protobuf.util.JsonFormat;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.kafka.TbKafkaDecoder;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
/**
* Created by ashvayka on 25.09.18.
@@ -27,6 +29,8 @@ public class RemoteJsResponseDecoder implements TbKafkaDecoder executeUpdateAsync(TbMsg msg) {
+ ListenableFuture result = executeScriptAsync(msg);
+ return Futures.transformAsync(result, json -> {
+ if (!json.isObject()) {
+ log.warn("Wrong result type: {}", json.getNodeType());
+ return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
+ } else {
+ return Futures.immediateFuture(unbindMsg(json, msg));
+ }
+ });
+ }
+
@Override
public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
JsonNode result = executeScript(prevMsg);
@@ -144,6 +159,19 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return result.asBoolean();
}
+ @Override
+ public ListenableFuture executeFilterAsync(TbMsg msg) {
+ ListenableFuture result = executeScriptAsync(msg);
+ return Futures.transformAsync(result, json -> {
+ if (!json.isBoolean()) {
+ log.warn("Wrong result type: {}", json.getNodeType());
+ return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
+ } else {
+ return Futures.immediateFuture(json.asBoolean());
+ }
+ });
+ }
+
@Override
public Set executeSwitch(TbMsg msg) throws ScriptException {
JsonNode result = executeScript(msg);
@@ -173,7 +201,7 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return mapper.readTree(eval);
} catch (ExecutionException e) {
if (e.getCause() instanceof ScriptException) {
- throw (ScriptException)e.getCause();
+ throw (ScriptException) e.getCause();
} else if (e.getCause() instanceof RuntimeException) {
throw new ScriptException(e.getCause().getMessage());
} else {
@@ -184,6 +212,24 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
}
}
+ private ListenableFuture executeScriptAsync(TbMsg msg) {
+ String[] inArgs = prepareArgs(msg);
+ return Futures.transformAsync(sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]),
+ o -> {
+ try {
+ return Futures.immediateFuture(mapper.readTree(o.toString()));
+ } catch (Exception e) {
+ if (e.getCause() instanceof ScriptException) {
+ return Futures.immediateFailedFuture(e.getCause());
+ } else if (e.getCause() instanceof RuntimeException) {
+ return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage()));
+ } else {
+ return Futures.immediateFailedFuture(new ScriptException(e));
+ }
+ }
+ });
+ }
+
public void destroy() {
sandboxService.release(this.scriptId);
}
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 94d0fea86f..3eb4d115e3 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
@@ -42,14 +42,13 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.dao.audit.AuditLogService;
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.server.service.security.exception.UserPasswordExpiredException;
-import org.thingsboard.server.service.security.model.SecuritySettings;
-import org.thingsboard.server.service.security.model.UserPasswordPolicy;
+import org.thingsboard.server.common.data.security.model.SecuritySettings;
+import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import javax.annotation.Resource;
import java.util.ArrayList;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
index 50265863b3..1425600cc4 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
@@ -19,7 +19,7 @@ import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.service.security.model.SecuritySettings;
+import org.thingsboard.server.common.data.security.model.SecuritySettings;
public interface SystemSecurityService {
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 37d6473d53..c3c8dc7803 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
@@ -30,7 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
-import org.thingsboard.rule.engine.api.RpcError;
+import org.springframework.util.StringUtils;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
@@ -38,8 +38,13 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@@ -49,22 +54,16 @@ import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
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.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
-import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
@@ -92,7 +91,8 @@ public class DefaultDeviceStateService implements DeviceStateService {
public static final String INACTIVITY_ALARM_TIME = "inactivityAlarmTime";
public static final String INACTIVITY_TIMEOUT = "inactivityTimeout";
- public static final List PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME, LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT);
+ public static final List PERSISTENT_ATTRIBUTES = Arrays.asList(ACTIVITY_STATE, LAST_CONNECT_TIME,
+ LAST_DISCONNECT_TIME, LAST_ACTIVITY_TIME, INACTIVITY_ALARM_TIME, INACTIVITY_TIMEOUT);
@Autowired
private TenantService tenantService;
@@ -103,6 +103,9 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Autowired
private AttributesService attributesService;
+ @Autowired
+ private TimeseriesService tsService;
+
@Autowired
@Lazy
private ActorService actorService;
@@ -122,19 +125,19 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Value("${state.defaultStateCheckIntervalInSec}")
@Getter
- private long defaultStateCheckIntervalInSec;
+ private int defaultStateCheckIntervalInSec;
-// TODO in v2.1
-// @Value("${state.defaultStatePersistenceIntervalInSec}")
-// @Getter
-// private long defaultStatePersistenceIntervalInSec;
-//
-// @Value("${state.defaultStatePersistencePack}")
-// @Getter
-// private long defaultStatePersistencePack;
+ @Value("${state.persistToTelemetry:false}")
+ @Getter
+ private boolean persistToTelemetry;
- private ListeningScheduledExecutorService queueExecutor;
+ @Value("${state.initFetchPackSize:1000}")
+ @Getter
+ private int initFetchPackSize;
+ private volatile boolean clusterUpdatePending = false;
+
+ private ListeningScheduledExecutorService queueExecutor;
private ConcurrentMap> tenantDevices = new ConcurrentHashMap<>();
private ConcurrentMap deviceStates = new ConcurrentHashMap<>();
@@ -143,7 +146,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
// Should be always single threaded due to absence of locks.
queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor());
queueExecutor.submit(this::initStateFromDB);
- queueExecutor.scheduleAtFixedRate(this::updateState, defaultStateCheckIntervalInSec, defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
+ queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
//TODO: schedule persistence in v2.1;
}
@@ -191,7 +194,10 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Override
public void onClusterUpdate() {
- queueExecutor.submit(this::onClusterUpdateSync);
+ if (!clusterUpdatePending) {
+ clusterUpdatePending = true;
+ queueExecutor.submit(this::onClusterUpdateSync);
+ }
}
@Override
@@ -219,46 +225,59 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
private void onClusterUpdateSync() {
+ clusterUpdatePending = false;
List tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
for (Tenant tenant : tenants) {
List> fetchFutures = new ArrayList<>();
- List devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
- for (Device device : devices) {
- if (!routingService.resolveById(device.getId()).isPresent()) {
- if (!deviceStates.containsKey(device.getId())) {
- fetchFutures.add(fetchDeviceState(device));
+ TextPageLink pageLink = new TextPageLink(initFetchPackSize);
+ while (pageLink != null) {
+ TextPageData page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink);
+ pageLink = page.getNextPageLink();
+ for (Device device : page.getData()) {
+ if (!routingService.resolveById(device.getId()).isPresent()) {
+ if (!deviceStates.containsKey(device.getId())) {
+ fetchFutures.add(fetchDeviceState(device));
+ }
+ } else {
+ Set tenantDeviceSet = tenantDevices.get(tenant.getId());
+ if (tenantDeviceSet != null) {
+ tenantDeviceSet.remove(device.getId());
+ }
+ deviceStates.remove(device.getId());
}
- } else {
- Set tenantDeviceSet = tenantDevices.get(tenant.getId());
- if (tenantDeviceSet != null) {
- tenantDeviceSet.remove(device.getId());
- }
- deviceStates.remove(device.getId());
}
- }
- try {
- Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
- } catch (InterruptedException | ExecutionException e) {
- log.warn("Failed to init device state service from DB", e);
+ try {
+ Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
+ } catch (InterruptedException | ExecutionException e) {
+ log.warn("Failed to init device state service from DB", e);
+ }
}
}
}
private void initStateFromDB() {
- List tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
- for (Tenant tenant : tenants) {
- List> fetchFutures = new ArrayList<>();
- List devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
- for (Device device : devices) {
- if (!routingService.resolveById(device.getId()).isPresent()) {
- fetchFutures.add(fetchDeviceState(device));
+ try {
+ List tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
+ for (Tenant tenant : tenants) {
+ List> fetchFutures = new ArrayList<>();
+ TextPageLink pageLink = new TextPageLink(initFetchPackSize);
+ while (pageLink != null) {
+ TextPageData page = deviceService.findDevicesByTenantId(tenant.getId(), pageLink);
+ pageLink = page.getNextPageLink();
+ for (Device device : page.getData()) {
+ if (!routingService.resolveById(device.getId()).isPresent()) {
+ fetchFutures.add(fetchDeviceState(device));
+ }
+ }
+ try {
+ Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
+ } catch (InterruptedException | ExecutionException e) {
+ log.warn("Failed to init device state service from DB", e);
+ }
}
}
- try {
- Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
- } catch (InterruptedException | ExecutionException e) {
- log.warn("Failed to init device state service from DB", e);
- }
+ } catch (Throwable t) {
+ log.warn("Failed to init device states from DB", t);
}
}
@@ -270,15 +289,21 @@ public class DefaultDeviceStateService implements DeviceStateService {
private void updateState() {
long ts = System.currentTimeMillis();
Set deviceIds = new HashSet<>(deviceStates.keySet());
+ log.info("Calculating state updates for {} devices", deviceStates.size());
for (DeviceId deviceId : deviceIds) {
- DeviceStateData stateData = deviceStates.get(deviceId);
- DeviceState state = stateData.getState();
- state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
- if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) {
- state.setLastInactivityAlarmTime(ts);
- pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
- saveAttribute(deviceId, INACTIVITY_ALARM_TIME, ts);
- saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+ DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
+ if (stateData != null) {
+ DeviceState state = stateData.getState();
+ state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
+ if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) {
+ state.setLastInactivityAlarmTime(ts);
+ pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
+ save(deviceId, INACTIVITY_ALARM_TIME, ts);
+ save(deviceId, ACTIVITY_STATE, state.isActive());
+ }
+ } else {
+ log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId);
+ deviceStates.remove(deviceId);
}
}
}
@@ -289,7 +314,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
long ts = System.currentTimeMillis();
stateData.getState().setLastConnectTime(ts);
pushRuleEngineMessage(stateData, CONNECT_EVENT);
- saveAttribute(deviceId, LAST_CONNECT_TIME, ts);
+ save(deviceId, LAST_CONNECT_TIME, ts);
}
}
@@ -299,7 +324,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
long ts = System.currentTimeMillis();
stateData.getState().setLastDisconnectTime(ts);
pushRuleEngineMessage(stateData, DISCONNECT_EVENT);
- saveAttribute(deviceId, LAST_DISCONNECT_TIME, ts);
+ save(deviceId, LAST_DISCONNECT_TIME, ts);
}
}
@@ -308,11 +333,14 @@ public class DefaultDeviceStateService implements DeviceStateService {
if (stateData != null) {
DeviceState state = stateData.getState();
long ts = System.currentTimeMillis();
- state.setActive(true);
stateData.getState().setLastActivityTime(ts);
pushRuleEngineMessage(stateData, ACTIVITY_EVENT);
- saveAttribute(deviceId, LAST_ACTIVITY_TIME, ts);
- saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+ save(deviceId, LAST_ACTIVITY_TIME, ts);
+
+ if (!state.isActive()) {
+ state.setActive(true);
+ save(deviceId, ACTIVITY_STATE, state.isActive());
+ }
}
}
@@ -324,6 +352,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
if (device != null) {
try {
deviceStateData = fetchDeviceState(device).get();
+ deviceStates.putIfAbsent(deviceId, deviceStateData);
} catch (InterruptedException | ExecutionException e) {
log.debug("[{}] Failed to fetch device state!", deviceId, e);
}
@@ -345,7 +374,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
boolean oldActive = state.isActive();
state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
if (!oldActive && state.isActive() || oldActive && !state.isActive()) {
- saveAttribute(deviceId, ACTIVITY_STATE, state.isActive());
+ save(deviceId, ACTIVITY_STATE, state.isActive());
}
}
}
@@ -414,39 +443,55 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
private ListenableFuture fetchDeviceState(Device device) {
- ListenableFuture> attributes = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
- return Futures.transform(attributes, new Function, DeviceStateData>() {
+ if (persistToTelemetry) {
+ ListenableFuture> tsData = tsService.findLatest(TenantId.SYS_TENANT_ID, device.getId(), PERSISTENT_ATTRIBUTES);
+ return Futures.transform(tsData, extractDeviceStateData(device));
+ } else {
+ ListenableFuture> attrData = attributesService.find(TenantId.SYS_TENANT_ID, device.getId(), DataConstants.SERVER_SCOPE, PERSISTENT_ATTRIBUTES);
+ return Futures.transform(attrData, extractDeviceStateData(device));
+ }
+ }
+
+ private Function, DeviceStateData> extractDeviceStateData(Device device) {
+ return new Function, DeviceStateData>() {
@Nullable
@Override
- public DeviceStateData apply(@Nullable List attributes) {
- long lastActivityTime = getAttributeValue(attributes, LAST_ACTIVITY_TIME, 0L);
- long inactivityAlarmTime = getAttributeValue(attributes, INACTIVITY_ALARM_TIME, 0L);
- long inactivityTimeout = getAttributeValue(attributes, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
- boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout;
- DeviceState deviceState = DeviceState.builder()
- .active(active)
- .lastConnectTime(getAttributeValue(attributes, LAST_CONNECT_TIME, 0L))
- .lastDisconnectTime(getAttributeValue(attributes, LAST_DISCONNECT_TIME, 0L))
- .lastActivityTime(lastActivityTime)
- .lastInactivityAlarmTime(inactivityAlarmTime)
- .inactivityTimeout(inactivityTimeout)
- .build();
- TbMsgMetaData md = new TbMsgMetaData();
- md.putValue("deviceName", device.getName());
- md.putValue("deviceType", device.getType());
- return DeviceStateData.builder()
- .tenantId(device.getTenantId())
- .deviceId(device.getId())
- .metaData(md)
- .state(deviceState).build();
+ public DeviceStateData apply(@Nullable List data) {
+ try {
+ long lastActivityTime = getEntryValue(data, LAST_ACTIVITY_TIME, 0L);
+ long inactivityAlarmTime = getEntryValue(data, INACTIVITY_ALARM_TIME, 0L);
+ long inactivityTimeout = getEntryValue(data, INACTIVITY_TIMEOUT, TimeUnit.SECONDS.toMillis(defaultInactivityTimeoutInSec));
+ boolean active = System.currentTimeMillis() < lastActivityTime + inactivityTimeout;
+ DeviceState deviceState = DeviceState.builder()
+ .active(active)
+ .lastConnectTime(getEntryValue(data, LAST_CONNECT_TIME, 0L))
+ .lastDisconnectTime(getEntryValue(data, LAST_DISCONNECT_TIME, 0L))
+ .lastActivityTime(lastActivityTime)
+ .lastInactivityAlarmTime(inactivityAlarmTime)
+ .inactivityTimeout(inactivityTimeout)
+ .build();
+ TbMsgMetaData md = new TbMsgMetaData();
+ md.putValue("deviceName", device.getName());
+ md.putValue("deviceType", device.getType());
+ return DeviceStateData.builder()
+ .tenantId(device.getTenantId())
+ .deviceId(device.getId())
+ .metaData(md)
+ .state(deviceState).build();
+ } catch (Exception e) {
+ log.warn("[{}] Failed to fetch device state data", device.getId(), e);
+ throw new RuntimeException(e);
+ }
}
- });
+ };
}
- private long getAttributeValue(List attributes, String attributeName, long defaultValue) {
- for (AttributeKvEntry attribute : attributes) {
- if (attribute.getKey().equals(attributeName)) {
- return attribute.getLongValue().orElse(defaultValue);
+ private long getEntryValue(List extends KvEntry> kvEntries, String attributeName, long defaultValue) {
+ if (kvEntries != null) {
+ for (KvEntry entry : kvEntries) {
+ if (entry != null && !StringUtils.isEmpty(entry.getKey()) && entry.getKey().equals(attributeName)) {
+ return entry.getLongValue().orElse(defaultValue);
+ }
}
}
return defaultValue;
@@ -464,12 +509,26 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
}
- private void saveAttribute(DeviceId deviceId, String key, long value) {
- tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+ private void save(DeviceId deviceId, String key, long value) {
+ if (persistToTelemetry) {
+ tsSubService.saveAndNotify(
+ TenantId.SYS_TENANT_ID, deviceId,
+ Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))),
+ new AttributeSaveCallback(deviceId, key, value));
+ } else {
+ tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+ }
}
- private void saveAttribute(DeviceId deviceId, String key, boolean value) {
- tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+ private void save(DeviceId deviceId, String key, boolean value) {
+ if (persistToTelemetry) {
+ tsSubService.saveAndNotify(
+ TenantId.SYS_TENANT_ID, deviceId,
+ Collections.singletonList(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(key, value))),
+ new AttributeSaveCallback(deviceId, key, value));
+ } else {
+ tsSubService.saveAttrAndNotify(TenantId.SYS_TENANT_ID, deviceId, DataConstants.SERVER_SCOPE, key, value, new AttributeSaveCallback(deviceId, key, value));
+ }
}
private class AttributeSaveCallback implements FutureCallback {
diff --git a/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java b/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java
index 432d0b2bc6..b40e2b93fd 100644
--- a/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transaction/BaseRuleChainTransactionService.java
@@ -15,14 +15,12 @@
*/
package org.thingsboard.server.service.transaction;
-import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RuleChainTransactionService;
import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
@@ -34,7 +32,6 @@ import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Optional;
import java.util.Queue;
-import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
@@ -111,29 +108,18 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ
@Override
public void endTransaction(TbMsg msg, Consumer onSuccess, Consumer onFailure) {
- EntityId originatorId = msg.getTransactionData().getOriginatorId();
- UUID transactionId = msg.getTransactionData().getTransactionId();
-
- Optional address = routingService.resolveById(originatorId);
+ Optional address = routingService.resolveById(msg.getTransactionData().getOriginatorId());
if (address.isPresent()) {
- sendTransactionEventToRemoteServer(originatorId, transactionId, address.get());
+ sendTransactionEventToRemoteServer(msg, address.get());
executeOnSuccess(onSuccess, msg);
} else {
- endLocalTransaction(transactionId, originatorId, onSuccess, onFailure);
+ endLocalTransaction(msg, onSuccess, onFailure);
}
}
@Override
public void onRemoteTransactionMsg(ServerAddress serverAddress, byte[] data) {
- ClusterAPIProtos.TransactionEndServiceMsgProto proto;
- try {
- proto = ClusterAPIProtos.TransactionEndServiceMsgProto.parseFrom(data);
- } catch (InvalidProtocolBufferException e) {
- throw new RuntimeException(e);
- }
- EntityId originatorId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getOriginatorIdMSB(), proto.getOriginatorIdLSB()));
- UUID transactionId = new UUID(proto.getTransactionIdMSB(), proto.getTransactionIdLSB());
- endLocalTransaction(transactionId, originatorId, msg -> {
+ endLocalTransaction(TbMsg.fromBytes(data), msg -> {
}, error -> {
});
}
@@ -144,21 +130,21 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ
log.trace("Added msg to queue, size: [{}]", queue.size());
}
- private void endLocalTransaction(UUID transactionId, EntityId originatorId, Consumer onSuccess, Consumer onFailure) {
+ private void endLocalTransaction(TbMsg msg, Consumer onSuccess, Consumer onFailure) {
transactionLock.lock();
try {
- BlockingQueue queue = transactionMap.computeIfAbsent(originatorId, id ->
+ BlockingQueue queue = transactionMap.computeIfAbsent(msg.getTransactionData().getOriginatorId(), id ->
new LinkedBlockingQueue<>(finalQueueSize));
TbTransactionTask currentTransactionTask = queue.peek();
if (currentTransactionTask != null) {
- if (currentTransactionTask.getMsg().getTransactionData().getTransactionId().equals(transactionId)) {
+ if (currentTransactionTask.getMsg().getTransactionData().getTransactionId().equals(msg.getTransactionData().getTransactionId())) {
currentTransactionTask.setCompleted(true);
queue.poll();
log.trace("Removed msg from queue, size [{}]", queue.size());
executeOnSuccess(currentTransactionTask.getOnEnd(), currentTransactionTask.getMsg());
- executeOnSuccess(onSuccess, currentTransactionTask.getMsg());
+ executeOnSuccess(onSuccess, msg);
TbTransactionTask nextTransactionTask = queue.peek();
if (nextTransactionTask != null) {
@@ -247,14 +233,8 @@ public class BaseRuleChainTransactionService implements RuleChainTransactionServ
callbackExecutor.executeAsync(task);
}
- private void sendTransactionEventToRemoteServer(EntityId entityId, UUID transactionId, ServerAddress address) {
- log.trace("[{}][{}] Originator is monitored on other server: {}", entityId, transactionId, address);
- ClusterAPIProtos.TransactionEndServiceMsgProto.Builder builder = ClusterAPIProtos.TransactionEndServiceMsgProto.newBuilder();
- builder.setEntityType(entityId.getEntityType().name());
- builder.setOriginatorIdMSB(entityId.getId().getMostSignificantBits());
- builder.setOriginatorIdLSB(entityId.getId().getLeastSignificantBits());
- builder.setTransactionIdMSB(transactionId.getMostSignificantBits());
- builder.setTransactionIdLSB(transactionId.getLeastSignificantBits());
- clusterRpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TRANSACTION_SERVICE_MESSAGE, builder.build().toByteArray());
+ private void sendTransactionEventToRemoteServer(TbMsg msg, ServerAddress address) {
+ log.trace("[{}][{}] Originator is monitored on other server: {}", msg.getTransactionData().getOriginatorId(), msg.getTransactionData().getTransactionId(), address);
+ clusterRpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_TRANSACTION_SERVICE_MESSAGE, TbMsg.toByteArray(msg));
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java
index 4c0f9bdfee..1c1c7be483 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/RemoteRuleEngineTransportService.java
@@ -16,7 +16,6 @@
package org.thingsboard.server.service.transport;
import akka.actor.ActorRef;
-import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.BlockingBucket;
import io.github.bucket4j.Bucket4j;
@@ -30,11 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
-import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceActorToTransportMsg;
import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg;
@@ -44,7 +42,6 @@ import org.thingsboard.server.kafka.TBKafkaConsumerTemplate;
import org.thingsboard.server.kafka.TBKafkaProducerTemplate;
import org.thingsboard.server.kafka.TbKafkaSettings;
import org.thingsboard.server.kafka.TbNodeIdProvider;
-import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
@@ -83,6 +80,8 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
private long pollRecordsPerSecond;
@Value("${transport.remote.rule_engine.max_poll_records_per_minute}")
private long pollRecordsPerMinute;
+ @Value("${transport.remote.rule_engine.stats.enabled:false}")
+ private boolean statsEnabled;
@Autowired
private TbKafkaSettings kafkaSettings;
@@ -108,6 +107,8 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
private volatile boolean stopped = false;
+ private final RuleEngineStats stats = new RuleEngineStats();
+
@PostConstruct
public void init() {
TBKafkaProducerTemplate.TBKafkaProducerTemplateBuilder notificationsProducerBuilder = TBKafkaProducerTemplate.builder();
@@ -176,6 +177,13 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
});
}
+ @Scheduled(fixedDelayString = "${transport.remote.rule_engine.stats.print_interval_ms}")
+ public void printStats() {
+ if (statsEnabled) {
+ stats.printStats();
+ }
+ }
+
@Override
public void process(String nodeId, DeviceActorToTransportMsg msg) {
process(nodeId, msg, null, null);
@@ -191,6 +199,9 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
}
private void forwardToDeviceActor(TransportToDeviceActorMsg toDeviceActorMsg) {
+ if (statsEnabled) {
+ stats.log(toDeviceActorMsg);
+ }
TransportToDeviceActorMsgWrapper wrapper = new TransportToDeviceActorMsgWrapper(toDeviceActorMsg);
Optional address = routingService.resolveById(wrapper.getDeviceId());
if (address.isPresent()) {
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java b/application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java
new file mode 100644
index 0000000000..13cae48383
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/transport/RuleEngineStats.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright © 2016-2019 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.transport;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.gen.transport.TransportProtos;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Slf4j
+public class RuleEngineStats {
+
+ private final AtomicInteger totalCounter = new AtomicInteger(0);
+ private final AtomicInteger sessionEventCounter = new AtomicInteger(0);
+ private final AtomicInteger postTelemetryCounter = new AtomicInteger(0);
+ private final AtomicInteger postAttributesCounter = new AtomicInteger(0);
+ private final AtomicInteger getAttributesCounter = new AtomicInteger(0);
+ private final AtomicInteger subscribeToAttributesCounter = new AtomicInteger(0);
+ private final AtomicInteger subscribeToRPCCounter = new AtomicInteger(0);
+ private final AtomicInteger toDeviceRPCCallResponseCounter = new AtomicInteger(0);
+ private final AtomicInteger toServerRPCCallRequestCounter = new AtomicInteger(0);
+ private final AtomicInteger subscriptionInfoCounter = new AtomicInteger(0);
+ private final AtomicInteger claimDeviceCounter = new AtomicInteger(0);
+
+ public void log(TransportProtos.TransportToDeviceActorMsg msg) {
+ totalCounter.incrementAndGet();
+ if (msg.hasSessionEvent()) {
+ sessionEventCounter.incrementAndGet();
+ }
+ if (msg.hasPostTelemetry()) {
+ postTelemetryCounter.incrementAndGet();
+ }
+ if (msg.hasPostAttributes()) {
+ postAttributesCounter.incrementAndGet();
+ }
+ if (msg.hasGetAttributes()) {
+ getAttributesCounter.incrementAndGet();
+ }
+ if (msg.hasSubscribeToAttributes()) {
+ subscribeToAttributesCounter.incrementAndGet();
+ }
+ if (msg.hasSubscribeToRPC()) {
+ subscribeToRPCCounter.incrementAndGet();
+ }
+ if (msg.hasToDeviceRPCCallResponse()) {
+ toDeviceRPCCallResponseCounter.incrementAndGet();
+ }
+ if (msg.hasToServerRPCCallRequest()) {
+ toServerRPCCallRequestCounter.incrementAndGet();
+ }
+ if (msg.hasSubscriptionInfo()) {
+ subscriptionInfoCounter.incrementAndGet();
+ }
+ if (msg.hasClaimDevice()) {
+ claimDeviceCounter.incrementAndGet();
+ }
+ }
+
+ public void printStats() {
+ int total = totalCounter.getAndSet(0);
+ if (total > 0) {
+ log.info("Transport total [{}] sessionEvents [{}] telemetry [{}] attributes [{}] getAttr [{}] subToAttr [{}] subToRpc [{}] toDevRpc [{}] " +
+ "toServerRpc [{}] subInfo [{}] claimDevice [{}] ",
+ total, sessionEventCounter.getAndSet(0), postTelemetryCounter.getAndSet(0),
+ postAttributesCounter.getAndSet(0), getAttributesCounter.getAndSet(0), subscribeToAttributesCounter.getAndSet(0),
+ subscribeToRPCCounter.getAndSet(0), toDeviceRPCCallResponseCounter.getAndSet(0),
+ toServerRPCCallRequestCounter.getAndSet(0), subscriptionInfoCounter.getAndSet(0), claimDeviceCounter.getAndSet(0));
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
index 4a6fa6f271..14cbf95861 100644
--- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
@@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
-import org.thingsboard.server.service.update.model.UpdateMessage;
+import org.thingsboard.server.common.data.UpdateMessage;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
diff --git a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java
index 4ddc6dc3df..016c573399 100644
--- a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java
@@ -15,7 +15,7 @@
*/
package org.thingsboard.server.service.update;
-import org.thingsboard.server.service.update.model.UpdateMessage;
+import org.thingsboard.server.common.data.UpdateMessage;
public interface UpdateService {
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 7a4648f234..67fbf15be5 100644
--- a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
+++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
@@ -19,6 +19,7 @@ import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.charset.Charset;
+import java.util.Random;
/**
diff --git a/application/src/main/proto/cluster.proto b/application/src/main/proto/cluster.proto
index b04a95fbb0..4ff1359e76 100644
--- a/application/src/main/proto/cluster.proto
+++ b/application/src/main/proto/cluster.proto
@@ -143,11 +143,3 @@ message DeviceStateServiceMsgProto {
bool updated = 6;
bool deleted = 7;
}
-
-message TransactionEndServiceMsgProto {
- string entityType = 1;
- int64 originatorIdMSB = 2;
- int64 originatorIdLSB = 3;
- int64 transactionIdMSB = 4;
- int64 transactionIdLSB = 5;
-}
diff --git a/application/src/main/proto/jsinvoke.proto b/application/src/main/proto/jsinvoke.proto
index d42f3c06c0..4c95acadbd 100644
--- a/application/src/main/proto/jsinvoke.proto
+++ b/application/src/main/proto/jsinvoke.proto
@@ -26,12 +26,9 @@ enum JsInvokeErrorCode {
}
message RemoteJsRequest {
- string responseTopic = 1;
- int64 requestIdMSB = 2;
- int64 requestIdLSB = 3;
- JsCompileRequest compileRequest = 4;
- JsInvokeRequest invokeRequest = 5;
- JsReleaseRequest releaseRequest = 6;
+ JsCompileRequest compileRequest = 1;
+ JsInvokeRequest invokeRequest = 2;
+ JsReleaseRequest releaseRequest = 3;
}
message RemoteJsResponse {
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 48e37dd646..fcf62a659b 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -85,6 +85,9 @@ cluster:
vitrual_nodes_size: "${CLUSTER_VIRTUAL_NODES_SIZE:16}"
# Queue partition id for current node
partition_id: "${QUEUE_PARTITION_ID:0}"
+ stats:
+ enabled: "${TB_CLUSTER_STATS_ENABLED:false}"
+ print_interval_ms: "${TB_CLUSTER_STATS_PRINT_INTERVAL_MS:10000}"
# Plugins configuration parameters
plugins:
@@ -183,6 +186,8 @@ cassandra:
rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:10000}"
# set all data types values except target to null for the same ts on save
set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:false}"
+ # log one of cassandra queries with specified frequency (0 - logging is disabled)
+ print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}"
tenant_rate_limits:
enabled: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_ENABLED:false}"
configuration: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION:1000:1,30000:60}"
@@ -194,6 +199,25 @@ sql:
ts_inserts_executor_type: "${SQL_TS_INSERTS_EXECUTOR_TYPE:fixed}"
# Specify thread pool size for FIXED executor service type
ts_inserts_fixed_thread_pool_size: "${SQL_TS_INSERTS_FIXED_THREAD_POOL_SIZE:200}"
+ # Specify batch size for persisting attribute updates
+ attributes:
+ batch_size: "${SQL_ATTRIBUTES_BATCH_SIZE:10000}"
+ batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}"
+ stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}"
+ ts:
+ batch_size: "${SQL_TS_BATCH_SIZE:10000}"
+ batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}"
+ stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}"
+ ts_latest:
+ batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}"
+ batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}"
+ stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}"
+ ts_timescale:
+ batch_size: "${SQL_TS_TIMESCALE_BATCH_SIZE:10000}"
+ batch_max_delay: "${SQL_TS_TIMESCALE_BATCH_MAX_DELAY_MS:100}"
+ stats_print_interval_ms: "${SQL_TS_TIMESCALE_BATCH_STATS_PRINT_MS:10000}"
+ # Specify whether to remove null characters from strValue of attributes and timeseries before insert
+ remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}"
# Actor system parameters
actors:
@@ -208,7 +232,7 @@ actors:
timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}"
rule:
# Specify thread pool size for database request callbacks executor service
- db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:1}"
+ db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:50}"
# Specify thread pool size for javascript executor service
js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:50}"
# Specify thread pool size for mail sender executor service
@@ -234,6 +258,7 @@ actors:
statistics:
# Enable/disable actor statistics
enabled: "${ACTORS_STATISTICS_ENABLED:true}"
+ js_print_interval_ms: "${ACTORS_JS_STATISTICS_PRINT_INTERVAL_MS:10000}"
persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:3600000}"
queue:
# Enable/disable persistence of un-processed messages to the queue
@@ -278,10 +303,41 @@ redis:
# standalone or cluster
connection:
type: standalone
+ standalone:
host: "${REDIS_HOST:localhost}"
port: "${REDIS_PORT:6379}"
- db: "${REDIS_DB:0}"
- password: "${REDIS_PASSWORD:}"
+ 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}"
# Check new version updates parameters
updates:
@@ -320,6 +376,7 @@ spring:
repositories:
enabled: "true"
jpa:
+ open-in-view: "false"
hibernate:
ddl-auto: "none"
database-platform: "${SPRING_JPA_DATABASE_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}"
@@ -371,6 +428,7 @@ audit-log:
state:
defaultInactivityTimeoutInSec: "${DEFAULT_INACTIVITY_TIMEOUT:10}"
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:10}"
+ persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
kafka:
enabled: true
@@ -403,7 +461,7 @@ js:
# 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
- max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:100}"
+ max_cpu_time: "${LOCAL_JS_SANDBOX_MAX_CPU_TIME:3000}"
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${LOCAL_JS_SANDBOX_MAX_ERRORS:3}"
# Remote JavaScript environment properties
@@ -422,6 +480,9 @@ js:
response_auto_commit_interval: "${REMOTE_JS_RESPONSE_AUTO_COMMIT_INTERVAL_MS:100}"
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}"
+ stats:
+ enabled: "${TB_JS_REMOTE_STATS_ENABLED:false}"
+ print_interval_ms: "${TB_JS_REMOTE_STATS_PRINT_INTERVAL_MS:10000}"
transport:
type: "${TRANSPORT_TYPE:local}" # local or remote
@@ -439,6 +500,9 @@ transport:
poll_records_pack_size: "${TB_RULE_ENGINE_MAX_POLL_RECORDS:1000}"
max_poll_records_per_second: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_SECOND:10000}"
max_poll_records_per_minute: "${TB_RULE_ENGINE_MAX_POLL_RECORDS_PER_MINUTE:120000}"
+ stats:
+ enabled: "${TB_RULE_ENGINE_STATS_ENABLED:false}"
+ print_interval_ms: "${TB_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:10000}"
notifications:
topic: "${TB_TRANSPORT_NOTIFICATIONS_TOPIC:tb.transport.notifications}"
sessions:
@@ -505,4 +569,4 @@ swagger:
license:
title: "${SWAGGER_LICENSE_TITLE:Apache License Version 2.0}"
url: "${SWAGGER_LICENSE_URL:https://github.com/thingsboard/thingsboard/blob/master/LICENSE}"
- version: "${SWAGGER_VERSION:2.0}"
\ No newline at end of file
+ version: "${SWAGGER_VERSION:2.0}"
diff --git a/application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java b/application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java
new file mode 100644
index 0000000000..ceff438b41
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingServiceTest.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016-2019 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.cluster.routing;
+
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.cluster.ServerType;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@Slf4j
+@RunWith(MockitoJUnitRunner.class)
+public class ConsistentClusterRoutingServiceTest {
+
+ private ConsistentClusterRoutingService clusterRoutingService;
+
+ private DiscoveryService discoveryService;
+
+ private String hashFunctionName = "murmur3_128";
+ private Integer virtualNodesSize = 1024*4;
+ private ServerAddress currentServer = new ServerAddress(" 100.96.1.0", 9001, ServerType.CORE);
+
+
+ @Before
+ public void setup() throws Exception {
+ discoveryService = mock(DiscoveryService.class);
+ clusterRoutingService = new ConsistentClusterRoutingService();
+ ReflectionTestUtils.setField(clusterRoutingService, "discoveryService", discoveryService);
+ ReflectionTestUtils.setField(clusterRoutingService, "hashFunctionName", hashFunctionName);
+ ReflectionTestUtils.setField(clusterRoutingService, "virtualNodesSize", virtualNodesSize);
+ when(discoveryService.getCurrentServer()).thenReturn(new ServerInstance(currentServer));
+ List otherServers = new ArrayList<>();
+ for (int i = 1; i < 30; i++) {
+ otherServers.add(new ServerInstance(new ServerAddress(" 100.96." + i*2 + "." + i, 9001, ServerType.CORE)));
+ }
+ when(discoveryService.getOtherServers()).thenReturn(otherServers);
+ clusterRoutingService.init();
+ }
+
+ @Test
+ public void testDispersionOnMillionDevices() {
+ List devices = new ArrayList<>();
+ for (int i = 0; i < 1000000; i++) {
+ devices.add(new DeviceId(UUIDs.timeBased()));
+ }
+
+ testDevicesDispersion(devices);
+ }
+
+ private void testDevicesDispersion(List devices) {
+ long start = System.currentTimeMillis();
+ Map map = new HashMap<>();
+ for (DeviceId deviceId : devices) {
+ ServerAddress address = clusterRoutingService.resolveById(deviceId).orElse(currentServer);
+ map.put(address, map.getOrDefault(address, 0) + 1);
+ }
+
+ List> data = map.entrySet().stream().sorted(Comparator.comparingInt(Map.Entry::getValue)).collect(Collectors.toList());
+ long end = System.currentTimeMillis();
+ System.out.println("Size: " + virtualNodesSize + " Time: " + (end - start) + " Diff: " + (data.get(data.size() - 1).getValue() - data.get(0).getValue()));
+
+ for (Map.Entry entry : data) {
+// System.out.println(entry.getKey().getHost() + ": " + entry.getValue());
+ }
+
+ }
+
+}
diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml
index b2a8cd467e..57cee90210 100644
--- a/common/dao-api/pom.xml
+++ b/common/dao-api/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
common
org.thingsboard.common
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java
similarity index 90%
rename from dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java
rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java
index eb8c800d1e..a5f2dbcab1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java
+++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesService.java
@@ -23,12 +23,13 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.device.claim.ClaimResult;
import java.util.List;
+import java.util.concurrent.ExecutionException;
public interface ClaimDevicesService {
ListenableFuture registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs);
- ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey);
+ ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) throws ExecutionException, InterruptedException;
ListenableFuture> reClaimDevice(TenantId tenantId, Device device);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java
similarity index 100%
rename from dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java
rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java
similarity index 100%
rename from dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java
rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResponse.java
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java
similarity index 100%
rename from dao/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java
rename to common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimResult.java
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 155a00b7f9..98b89d0d6f 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
common
org.thingsboard.common
diff --git a/application/src/main/java/org/thingsboard/server/controller/claim/data/ClaimRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java
similarity index 92%
rename from application/src/main/java/org/thingsboard/server/controller/claim/data/ClaimRequest.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java
index 6620aa187e..8845edb872 100644
--- a/application/src/main/java/org/thingsboard/server/controller/claim/data/ClaimRequest.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ClaimRequest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.controller.claim.data;
+package org.thingsboard.server.common.data;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java
similarity index 93%
rename from application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java
index b4e61f7bc2..19a0341dfe 100644
--- a/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.update.model;
+package org.thingsboard.server.common.data;
import lombok.Data;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
index 174044de28..c409cd2a37 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -31,6 +31,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements
private CustomerId customerId;
private String name;
private String type;
+ private String label;
public Asset() {
super();
@@ -46,6 +47,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements
this.customerId = asset.getCustomerId();
this.name = asset.getName();
this.type = asset.getType();
+ this.label = asset.getLabel();
}
public TenantId getTenantId() {
@@ -81,6 +83,14 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements
this.type = type;
}
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
@Override
public String getSearchText() {
return getName();
@@ -97,6 +107,8 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements
builder.append(name);
builder.append(", type=");
builder.append(type);
+ builder.append(", label=");
+ builder.append(label);
builder.append(", additionalInfo=");
builder.append(getAdditionalInfo());
builder.append(", createdTime=");
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
similarity index 93%
rename from application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
index bfc9176b4b..ee531fd4fd 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/SecuritySettings.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.security.model;
+package org.thingsboard.server.common.data.security.model;
import lombok.Data;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java
rename to common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java
index 0c50b5f6e0..9718c4ecb2 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/UserPasswordPolicy.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.security.model;
+package org.thingsboard.server.common.data.security.model;
import lombok.Data;
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 60702c0801..30d4b9d957 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
common
org.thingsboard.common
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 be346db040..cbc9b979bf 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
@@ -67,7 +67,7 @@ public final class TbMsg implements Serializable {
this(id, type, originator, metaData, dataType, data, new TbMsgTransactionData(id, originator), ruleChainId, ruleNodeId, clusterPartition);
}
- public static ByteBuffer toBytes(TbMsg msg) {
+ public static byte[] toByteArray(TbMsg msg) {
MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder();
builder.setId(msg.getId().toString());
builder.setType(msg.getType());
@@ -101,8 +101,16 @@ public final class TbMsg implements Serializable {
builder.setDataType(msg.getDataType().ordinal());
builder.setData(msg.getData());
- byte[] bytes = builder.build().toByteArray();
- return ByteBuffer.wrap(bytes);
+ return builder.build().toByteArray();
+
+ }
+
+ public static ByteBuffer toBytes(TbMsg msg) {
+ return ByteBuffer.wrap(toByteArray(msg));
+ }
+
+ public static TbMsg fromBytes(byte[] data) {
+ return fromBytes(ByteBuffer.wrap(data));
}
public static TbMsg fromBytes(ByteBuffer buffer) {
@@ -115,8 +123,8 @@ public final class TbMsg implements Serializable {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB()));
RuleChainId ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB()));
RuleNodeId ruleNodeId = null;
- if(proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
- ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
+ if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) {
+ ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB()));
}
TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, dataType, proto.getData(), transactionData, ruleChainId, ruleNodeId, proto.getClusterPartition());
diff --git a/common/pom.xml b/common/pom.xml
index ce20b753ba..29e2223405 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
thingsboard
common
diff --git a/common/queue/pom.xml b/common/queue/pom.xml
index 7821051137..d17f37ad9d 100644
--- a/common/queue/pom.xml
+++ b/common/queue/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
common
org.thingsboard.common
diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java
index bcb6797781..0762e78316 100644
--- a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java
+++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaProducerTemplate.java
@@ -49,9 +49,6 @@ public class TBKafkaProducerTemplate {
private final KafkaProducer producer;
private final TbKafkaEncoder encoder;
- @Builder.Default
- private TbKafkaEnricher enricher = ((value, responseTopic, requestId) -> value);
-
private final TbKafkaPartitioner partitioner;
private ConcurrentMap> partitionInfoMap;
@Getter
@@ -61,7 +58,7 @@ public class TBKafkaProducerTemplate {
private final TbKafkaSettings settings;
@Builder
- private TBKafkaProducerTemplate(TbKafkaSettings settings, TbKafkaEncoder encoder, TbKafkaEnricher enricher,
+ private TBKafkaProducerTemplate(TbKafkaSettings settings, TbKafkaEncoder encoder,
TbKafkaPartitioner partitioner, String defaultTopic, String clientId) {
Properties props = settings.toProps();
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
@@ -72,7 +69,6 @@ public class TBKafkaProducerTemplate {
this.settings = settings;
this.producer = new KafkaProducer<>(props);
this.encoder = encoder;
- this.enricher = enricher;
this.partitioner = partitioner;
this.defaultTopic = defaultTopic;
}
@@ -93,14 +89,6 @@ public class TBKafkaProducerTemplate {
}
}
- T enrich(T value, String responseTopic, UUID requestId) {
- if (enricher != null) {
- return enricher.enrich(value, responseTopic, requestId);
- } else {
- return value;
- }
- }
-
public Future send(String key, T value, Callback callback) {
return send(key, value, null, callback);
}
diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java
index 28c1e2cba2..461866e406 100644
--- a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java
+++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaRequestTemplate.java
@@ -144,11 +144,11 @@ public class TbKafkaRequestTemplate extends AbstractTbKafkaTe
tickSize = pendingRequests.size();
if (nextCleanupMs < tickTs) {
//cleanup;
- pendingRequests.entrySet().forEach(kv -> {
- if (kv.getValue().expTime < tickTs) {
- ResponseMetaData staleRequest = pendingRequests.remove(kv.getKey());
+ pendingRequests.forEach((key, value) -> {
+ if (value.expTime < tickTs) {
+ ResponseMetaData staleRequest = pendingRequests.remove(key);
if (staleRequest != null) {
- log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", kv.getKey(), staleRequest.expTime, tickTs);
+ log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs);
staleRequest.future.setException(new TimeoutException());
}
}
@@ -189,13 +189,12 @@ public class TbKafkaRequestTemplate extends AbstractTbKafkaTe
SettableFuture future = SettableFuture.create();
ResponseMetaData responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future);
pendingRequests.putIfAbsent(requestId, responseMetaData);
- request = requestTemplate.enrich(request, responseTemplate.getTopic(), requestId);
log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, key, responseMetaData.expTime);
requestTemplate.send(key, request, headers, (metadata, exception) -> {
if (exception != null) {
log.trace("[{}] Failed to post the request", requestId, exception);
} else {
- log.trace("[{}] Posted the request", requestId, metadata);
+ log.trace("[{}] Posted the request: {}", requestId, metadata);
}
});
return future;
diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java
index 24d53a846e..bef8244534 100644
--- a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java
+++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaSettings.java
@@ -32,9 +32,8 @@ import java.util.Properties;
@Component
public class TbKafkaSettings {
- public static final String REQUEST_ID_HEADER = "requestId";
- public static final String RESPONSE_TOPIC_HEADER = "responseTopic";
-
+ static final String REQUEST_ID_HEADER = "requestId";
+ static final String RESPONSE_TOPIC_HEADER = "responseTopic";
@Value("${kafka.bootstrap.servers}")
private String servers;
diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml
index 26d478b596..0861ce31e3 100644
--- a/common/transport/coap/pom.xml
+++ b/common/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 2.4.2-SNAPSHOT
+ 2.4.2
transport
org.thingsboard.common.transport
diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml
index 644df5a7b4..51f8e2f0df 100644
--- a/common/transport/http/pom.xml
+++ b/common/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 2.4.2-SNAPSHOT
+ 2.4.2
transport
org.thingsboard.common.transport
diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml
index ef0ae8eabd..7ae8c35afa 100644
--- a/common/transport/mqtt/pom.xml
+++ b/common/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.common
- 2.4.2-SNAPSHOT
+ 2.4.2
transport
org.thingsboard.common.transport
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 d92b04e584..aa3757c4d7 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
@@ -409,6 +409,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
private void processDisconnect(ChannelHandlerContext ctx) {
ctx.close();
+ log.info("[{}] Client disconnected!", sessionId);
if (deviceSessionCtx.isConnected()) {
transportService.process(sessionInfo, AbstractTransportService.getSessionEventMsg(SessionEvent.CLOSED), null);
transportService.deregisterSession(sessionInfo);
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index ca36845902..abf152f245 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
common
org.thingsboard.common
diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml
index 88bc2f64d8..2f8fa6614c 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
- 2.4.2-SNAPSHOT
+ 2.4.2
transport
org.thingsboard.common.transport
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 ad6baf0258..99e1f9b7df 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
@@ -47,7 +47,7 @@ public class TransportContext {
@PostConstruct
public void init() {
- executor = Executors.newCachedThreadPool();
+ executor = Executors.newWorkStealingPool(50);
}
@PreDestroy
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 24b1343491..af757c2c27 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
@@ -201,10 +201,10 @@ public class JsonConverter {
.setBoolV(value.getAsBoolean()).build());
} else if (value.isNumber()) {
result.add(buildNumericKeyValueProto(value, valueEntry.getKey()));
- } else {
+ } else if (!value.isJsonNull()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + value);
}
- } else {
+ } else if (!element.isJsonNull()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + element);
}
}
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java
index ec7f491fed..632ce16bc1 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/RemoteTransportService.java
@@ -320,28 +320,19 @@ public class RemoteTransportService extends AbstractTransportService {
send(sessionInfo, toRuleEngineMsg, callback);
}
- private static class TransportCallbackAdaptor implements Callback {
- private final TransportServiceCallback callback;
-
- TransportCallbackAdaptor(TransportServiceCallback callback) {
- this.callback = callback;
- }
-
- @Override
- public void onCompletion(RecordMetadata metadata, Exception exception) {
- if (exception == null) {
- if (callback != null) {
- callback.onSuccess(null);
- }
- } else {
- if (callback != null) {
- callback.onError(exception);
+ private void send(SessionInfoProto sessionInfo, ToRuleEngineMsg toRuleEngineMsg, TransportServiceCallback callback) {
+ ruleEngineProducer.send(getRoutingKey(sessionInfo), toRuleEngineMsg, (metadata, exception) -> {
+ if (callback != null) {
+ if (exception == null) {
+ this.transportCallbackExecutor.submit(() -> {
+ callback.onSuccess(null);
+ });
+ } else {
+ this.transportCallbackExecutor.submit(() -> {
+ callback.onError(exception);
+ });
}
}
- }
- }
-
- private void send(SessionInfoProto sessionInfo, ToRuleEngineMsg toRuleEngineMsg, TransportServiceCallback callback) {
- ruleEngineProducer.send(getRoutingKey(sessionInfo), toRuleEngineMsg, new TransportCallbackAdaptor(callback));
+ });
}
}
diff --git a/common/util/pom.xml b/common/util/pom.xml
index c695ac573e..d3ba8c32ee 100644
--- a/common/util/pom.xml
+++ b/common/util/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
common
org.thingsboard.common
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java b/common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java
similarity index 94%
rename from application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
rename to common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java
index 221915d02c..1f839edb80 100644
--- a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
+++ b/common/util/src/main/java/org/thingsboard/common/util/AbstractListeningExecutor.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.service.executors;
+package org.thingsboard.common.util;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
-import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.common.util.ListeningExecutor;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java b/common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java
similarity index 85%
rename from rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java
rename to common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java
index 99df8f30fd..a12cc269b3 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java
+++ b/common/util/src/main/java/org/thingsboard/common/util/ListeningExecutor.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.rule.engine.api;
+package org.thingsboard.common.util;
import com.google.common.util.concurrent.ListenableFuture;
@@ -24,4 +24,8 @@ public interface ListeningExecutor extends Executor {
ListenableFuture executeAsync(Callable task);
+ default ListenableFuture submit(Callable task) {
+ return executeAsync(task);
+ }
+
}
diff --git a/dao/pom.xml b/dao/pom.xml
index ba44133fb8..3e5e204419 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
thingsboard
dao
@@ -47,6 +47,10 @@
org.thingsboard.common
dao-api
+
+ org.thingsboard.common
+ util
+
org.slf4j
slf4j-api
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 d83b068cbe..8a4279fe7f 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
@@ -52,6 +52,7 @@ import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@@ -325,21 +326,21 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
private AlarmSeverity detectHighestSeverity(List alarms) {
if (!alarms.isEmpty()) {
List sorted = new ArrayList(alarms);
- sorted.sort((p1, p2) -> p1.getSeverity().compareTo(p2.getSeverity()));
+ sorted.sort(Comparator.comparing(Alarm::getSeverity));
return sorted.get(0).getSeverity();
} else {
return null;
}
}
- private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
+ private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) {
log.debug("Deleting Alarm relation: {}", alarmRelation);
- relationService.deleteRelationAsync(tenantId, alarmRelation).get();
+ relationService.deleteRelation(tenantId, alarmRelation);
}
- private void createRelation(TenantId tenantId, EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
+ private void createRelation(TenantId tenantId, EntityRelation alarmRelation) {
log.debug("Creating Alarm relation: {}", alarmRelation);
- relationService.saveRelationAsync(tenantId, alarmRelation).get();
+ relationService.saveRelation(tenantId, alarmRelation);
}
private Alarm merge(Alarm existing, Alarm alarm) {
@@ -376,28 +377,18 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
}
private void createAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status, boolean createAnyRelation) {
- try {
- if (createAnyRelation) {
- createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM));
- }
- createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
- createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
- createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
- } catch (ExecutionException | InterruptedException e) {
- log.warn("[{}] Failed to create relation. Status: [{}]", alarmId, status);
- throw new RuntimeException(e);
+ if (createAnyRelation) {
+ createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM));
}
+ createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
+ createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
+ createRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
}
private void deleteAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus status) {
- try {
- deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
- deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
- deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
- } catch (ExecutionException | InterruptedException e) {
- log.warn("[{}] Failed to delete relation. Status: [{}]", alarmId, status);
- throw new RuntimeException(e);
- }
+ deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.name(), RelationTypeGroup.ALARM));
+ deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getClearSearchStatus().name(), RelationTypeGroup.ALARM));
+ deleteRelation(tenantId, new EntityRelation(entityId, alarmId, ALARM_RELATION_PREFIX + status.getAckSearchStatus().name(), RelationTypeGroup.ALARM));
}
private void updateAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId, AlarmStatus oldStatus, AlarmStatus newStatus) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
index cf679a103f..8cd3c476f8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
@@ -20,6 +20,7 @@ import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
+import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@@ -28,6 +29,7 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
@@ -113,7 +115,22 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
public Asset saveAsset(Asset asset) {
log.trace("Executing saveAsset [{}]", asset);
assetValidator.validate(asset, Asset::getTenantId);
- return assetDao.save(asset.getTenantId(), asset);
+ Asset savedAsset;
+ if (!sqlDatabaseUsed) {
+ savedAsset = assetDao.save(asset.getTenantId(), asset);
+ } else {
+ try {
+ savedAsset = assetDao.save(asset.getTenantId(), asset);
+ } catch (Exception t) {
+ ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
+ if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("asset_name_unq_key")) {
+ throw new DataValidationException("Asset with such name already exists!");
+ } else {
+ throw t;
+ }
+ }
+ }
+ return savedAsset;
}
@Override
@@ -265,22 +282,26 @@ public class BaseAssetService extends AbstractEntityService implements AssetServ
@Override
protected void validateCreate(TenantId tenantId, Asset asset) {
- assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
- d -> {
- throw new DataValidationException("Asset with such name already exists!");
- }
- );
+ if (!sqlDatabaseUsed) {
+ assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
+ d -> {
+ throw new DataValidationException("Asset with such name already exists!");
+ }
+ );
+ }
}
@Override
protected void validateUpdate(TenantId tenantId, Asset asset) {
- assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
- d -> {
- if (!d.getId().equals(asset.getId())) {
- throw new DataValidationException("Asset with such name already exists!");
+ if (!sqlDatabaseUsed) {
+ assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
+ d -> {
+ if (!d.getId().equals(asset.getId())) {
+ throw new DataValidationException("Asset with such name already exists!");
+ }
}
- }
- );
+ );
+ }
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
index f1c4b5a8fc..5ed0222d70 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisCacheConfiguration.java
@@ -28,38 +28,58 @@ import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.thingsboard.server.common.data.id.EntityId;
+import redis.clients.jedis.JedisPoolConfig;
@Configuration
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis", matchIfMissing = false)
@EnableCaching
@Data
-public class TBRedisCacheConfiguration {
+public abstract class TBRedisCacheConfiguration {
- @Value("${redis.connection.host}")
- private String host;
+ @Value("${redis.pool_config.maxTotal}")
+ private int maxTotal;
- @Value("${redis.connection.port}")
- private Integer port;
+ @Value("${redis.pool_config.maxIdle}")
+ private int maxIdle;
- @Value("${redis.connection.db}")
- private Integer db;
+ @Value("${redis.pool_config.minIdle}")
+ private int minIdle;
- @Value("${redis.connection.password}")
- private String password;
+ @Value("${redis.pool_config.testOnBorrow}")
+ private boolean testOnBorrow;
+
+ @Value("${redis.pool_config.testOnReturn}")
+ private boolean testOnReturn;
+
+ @Value("${redis.pool_config.testWhileIdle}")
+ private boolean testWhileIdle;
+
+ @Value("${redis.pool_config.minEvictableMs}")
+ private long minEvictableMs;
+
+ @Value("${redis.pool_config.evictionRunsMs}")
+ private long evictionRunsMs;
+
+ @Value("${redis.pool_config.maxWaitMills}")
+ private long maxWaitMills;
+
+ @Value("${redis.pool_config.numberTestsPerEvictionRun}")
+ private int numberTestsPerEvictionRun;
+
+ @Value("${redis.pool_config.blockWhenExhausted}")
+ private boolean blockWhenExhausted;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
- JedisConnectionFactory factory = new JedisConnectionFactory();
- factory.setHostName(host);
- factory.setPort(port);
- factory.setDatabase(db);
- factory.setPassword(password);
- return factory;
+ return loadFactory();
}
+ protected abstract JedisConnectionFactory loadFactory();
+
@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
DefaultFormattingConversionService redisConversionService = new DefaultFormattingConversionService();
@@ -74,8 +94,31 @@ public class TBRedisCacheConfiguration {
return new PreviousDeviceCredentialsIdKeyGenerator();
}
+ @Bean
+ public RedisTemplate redisTemplate() {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(redisConnectionFactory());
+ return template;
+ }
+
private static void registerDefaultConverters(ConverterRegistry registry) {
Assert.notNull(registry, "ConverterRegistry must not be null!");
registry.addConverter(EntityId.class, String.class, EntityId::toString);
}
+
+ protected JedisPoolConfig buildPoolConfig() {
+ final JedisPoolConfig poolConfig = new JedisPoolConfig();
+ poolConfig.setMaxTotal(maxTotal);
+ poolConfig.setMaxIdle(maxIdle);
+ poolConfig.setMinIdle(minIdle);
+ poolConfig.setTestOnBorrow(testOnBorrow);
+ poolConfig.setTestOnReturn(testOnReturn);
+ poolConfig.setTestWhileIdle(testWhileIdle);
+ poolConfig.setMinEvictableIdleTimeMillis(minEvictableMs);
+ poolConfig.setTimeBetweenEvictionRunsMillis(evictionRunsMs);
+ poolConfig.setMaxWaitMillis(maxWaitMills);
+ poolConfig.setNumTestsPerEvictionRun(numberTestsPerEvictionRun);
+ poolConfig.setBlockWhenExhausted(blockWhenExhausted);
+ return poolConfig;
+ }
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java
new file mode 100644
index 0000000000..8405db3c7d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisClusterConfiguration.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016-2019 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.cache;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisClusterConfiguration;
+import org.springframework.data.redis.connection.RedisNode;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Configuration
+@ConditionalOnMissingBean(CaffeineCacheConfiguration.class)
+@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "cluster")
+public class TBRedisClusterConfiguration extends TBRedisCacheConfiguration {
+
+ private static final String COMMA = ",";
+ private static final String COLON = ":";
+
+ @Value("${redis.cluster.nodes}")
+ private String clusterNodes;
+
+ @Value("${redis.cluster.max-redirects}")
+ private Integer maxRedirects;
+
+ @Value("${redis.cluster.useDefaultPoolConfig}")
+ private boolean useDefaultPoolConfig;
+
+ @Value("${redis.password}")
+ private String password;
+
+ public JedisConnectionFactory loadFactory() {
+ RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
+ clusterConfiguration.setClusterNodes(getNodes(clusterNodes));
+ clusterConfiguration.setMaxRedirects(maxRedirects);
+ clusterConfiguration.setPassword(password);
+ if (useDefaultPoolConfig) {
+ return new JedisConnectionFactory(clusterConfiguration);
+ } else {
+ return new JedisConnectionFactory(clusterConfiguration, buildPoolConfig());
+ }
+ }
+
+ private List getNodes(String nodes) {
+ List result;
+ if (StringUtils.isBlank(nodes)) {
+ result = Collections.emptyList();
+ } else {
+ result = new ArrayList<>();
+ for (String hostPort : nodes.split(COMMA)) {
+ String host = hostPort.split(COLON)[0];
+ Integer port = Integer.valueOf(hostPort.split(COLON)[1]);
+ result.add(new RedisNode(host, port));
+ }
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java
new file mode 100644
index 0000000000..b09b0dd565
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/TBRedisStandaloneConfiguration.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016-2019 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.cache;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+
+import java.time.Duration;
+
+@Configuration
+@ConditionalOnMissingBean(CaffeineCacheConfiguration.class)
+@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone")
+public class TBRedisStandaloneConfiguration extends TBRedisCacheConfiguration {
+
+ @Value("${redis.standalone.host}")
+ private String host;
+
+ @Value("${redis.standalone.port}")
+ private Integer port;
+
+ @Value("${redis.standalone.clientName}")
+ private String clientName;
+
+ @Value("${redis.standalone.connectTimeout}")
+ private Long connectTimeout;
+
+ @Value("${redis.standalone.readTimeout}")
+ private Long readTimeout;
+
+ @Value("${redis.standalone.useDefaultClientConfig}")
+ private boolean useDefaultClientConfig;
+
+ @Value("${redis.standalone.usePoolConfig}")
+ private boolean usePoolConfig;
+
+ @Value("${redis.db}")
+ private Integer db;
+
+ @Value("${redis.password}")
+ private String password;
+
+ public JedisConnectionFactory loadFactory() {
+ RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
+ standaloneConfiguration.setHostName(host);
+ standaloneConfiguration.setPort(port);
+ standaloneConfiguration.setDatabase(db);
+ standaloneConfiguration.setPassword(password);
+ if (useDefaultClientConfig) {
+ return new JedisConnectionFactory(standaloneConfiguration);
+ } else {
+ return new JedisConnectionFactory(standaloneConfiguration, buildClientConfig());
+ }
+ }
+
+ private JedisClientConfiguration buildClientConfig() {
+ if (usePoolConfig) {
+ return JedisClientConfiguration.builder()
+ .clientName(clientName)
+ .connectTimeout(Duration.ofMillis(connectTimeout))
+ .readTimeout(Duration.ofMillis(readTimeout))
+ .usePooling().poolConfig(buildPoolConfig())
+ .build();
+ } else {
+ return JedisClientConfiguration.builder()
+ .clientName(clientName)
+ .connectTimeout(Duration.ofMillis(connectTimeout))
+ .readTimeout(Duration.ofMillis(readTimeout)).build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java
index b12e114d93..3886a9de65 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java
@@ -54,11 +54,7 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic
public ComponentDescriptor saveComponent(TenantId tenantId, ComponentDescriptor component) {
componentValidator.validate(component, data -> new TenantId(EntityId.NULL_UUID));
Optional result = componentDescriptorDao.saveIfNotExist(tenantId, component);
- if (result.isPresent()) {
- return result.get();
- } else {
- return componentDescriptorDao.findByClazz(tenantId, component.getClazz());
- }
+ return result.orElseGet(() -> componentDescriptorDao.findByClazz(tenantId, component.getClazz()));
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.java b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.java
new file mode 100644
index 0000000000..3b3194dbbf
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDataInfo.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2019 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.device;
+
+import lombok.Data;
+import org.thingsboard.server.dao.device.claim.ClaimData;
+
+import java.util.List;
+
+@Data
+public class ClaimDataInfo {
+
+ private final boolean fromCache;
+ private final List key;
+ private final ClaimData data;
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java
index 9000c91823..6955cd761c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/ClaimDevicesServiceImpl.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.device;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@@ -23,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId;
@@ -37,9 +39,12 @@ import org.thingsboard.server.dao.device.claim.ClaimResponse;
import org.thingsboard.server.dao.device.claim.ClaimResult;
import org.thingsboard.server.dao.model.ModelConstants;
+import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE;
@@ -48,6 +53,8 @@ import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CA
public class ClaimDevicesServiceImpl implements ClaimDevicesService {
private static final String CLAIM_ATTRIBUTE_NAME = "claimingAllowed";
+ private static final String CLAIM_DATA_ATTRIBUTE_NAME = "claimingData";
+ private static final ObjectMapper mapper = new ObjectMapper();
@Autowired
private DeviceService deviceService;
@@ -95,24 +102,45 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
});
}
- @Override
- public ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) {
+ private ClaimDataInfo getClaimData(Cache cache, Device device) throws ExecutionException, InterruptedException {
List key = constructCacheKey(device.getId());
+ ClaimData claimDataFromCache = cache.get(key, ClaimData.class);
+ if (claimDataFromCache != null) {
+ return new ClaimDataInfo(true, key, claimDataFromCache);
+ } else {
+ Optional claimDataAttr = attributesService.find(device.getTenantId(), device.getId(),
+ DataConstants.SERVER_SCOPE, CLAIM_DATA_ATTRIBUTE_NAME).get();
+ if (claimDataAttr.isPresent()) {
+ try {
+ ClaimData claimDataFromAttribute = mapper.readValue(claimDataAttr.get().getValueAsString(), ClaimData.class);
+ return new ClaimDataInfo(false, key, claimDataFromAttribute);
+ } catch (IOException e) {
+ log.warn("Failed to read Claim Data [{}] from attribute!", claimDataAttr, e);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public ListenableFuture claimDevice(Device device, CustomerId customerId, String secretKey) throws ExecutionException, InterruptedException {
Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE);
- ClaimData claimData = cache.get(key, ClaimData.class);
+ ClaimDataInfo claimData = getClaimData(cache, device);
if (claimData != null) {
long currTs = System.currentTimeMillis();
- if (currTs > claimData.getExpirationTime() || !secretKey.equals(claimData.getSecretKey())) {
+ if (currTs > claimData.getData().getExpirationTime() || !secretKeyIsEmptyOrEqual(secretKey, claimData.getData().getSecretKey())) {
log.warn("The claiming timeout occurred or wrong 'secretKey' provided for the device [{}]", device.getName());
- cache.evict(key);
+ if (claimData.isFromCache()) {
+ cache.evict(claimData.getKey());
+ }
return Futures.immediateFuture(new ClaimResult(null, ClaimResponse.FAILURE));
} else {
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
device.setCustomerId(customerId);
Device savedDevice = deviceService.saveDevice(device);
- return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(savedDevice, ClaimResponse.SUCCESS));
+ return Futures.transform(removeClaimingSavedData(cache, claimData, device), result -> new ClaimResult(savedDevice, ClaimResponse.SUCCESS));
}
- return Futures.transform(removeClaimingSavedData(cache, key, device), result -> new ClaimResult(null, ClaimResponse.CLAIMED));
+ return Futures.transform(removeClaimingSavedData(cache, claimData, device), result -> new ClaimResult(null, ClaimResponse.CLAIMED));
}
} else {
log.warn("Failed to find the device's claiming message![{}]", device.getName());
@@ -124,6 +152,10 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
}
}
+ private boolean secretKeyIsEmptyOrEqual(String secretKeyA, String secretKeyB) {
+ return (StringUtils.isEmpty(secretKeyA) && StringUtils.isEmpty(secretKeyB)) || secretKeyA.equals(secretKeyB);
+ }
+
@Override
public ListenableFuture> reClaimDevice(TenantId tenantId, Device device) {
if (!device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
@@ -159,13 +191,12 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
return systemDurationMs;
}
- private ListenableFuture> removeClaimingSavedData(Cache cache, List key, Device device) {
- cache.evict(key);
- if (isAllowedClaimingByDefault) {
- return Futures.immediateFuture(null);
+ private ListenableFuture> removeClaimingSavedData(Cache cache, ClaimDataInfo data, Device device) {
+ if (data.isFromCache()) {
+ cache.evict(data.getKey());
}
return attributesService.removeAll(device.getTenantId(),
- device.getId(), DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME));
+ device.getId(), DataConstants.SERVER_SCOPE, Arrays.asList(CLAIM_ATTRIBUTE_NAME, CLAIM_DATA_ATTRIBUTE_NAME));
}
private void cacheEviction(DeviceId deviceId) {
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 7c7910ba8f..c14d25b65b 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
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
import lombok.extern.slf4j.Slf4j;
+import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
@@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
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;
@@ -38,7 +40,7 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
@Service
@Slf4j
-public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
+public class DeviceCredentialsServiceImpl extends AbstractEntityService implements DeviceCredentialsService {
@Autowired
private DeviceCredentialsDao deviceCredentialsDao;
@@ -78,7 +80,20 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
}
log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials);
credentialsValidator.validate(deviceCredentials, id -> tenantId);
- return deviceCredentialsDao.save(tenantId, deviceCredentials);
+ if (!sqlDatabaseUsed) {
+ return deviceCredentialsDao.save(tenantId, deviceCredentials);
+ } else {
+ try {
+ return deviceCredentialsDao.save(tenantId, deviceCredentials);
+ } catch (Exception t) {
+ ConstraintViolationException e = extractConstraintViolationException(t).orElse(null);
+ if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_credentials_id_unq_key")) {
+ throw new DataValidationException("Specified credentials are already registered!");
+ } else {
+ throw t;
+ }
+ }
+ }
}
private void formatCertData(DeviceCredentials deviceCredentials) {
@@ -100,9 +115,11 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
@Override
protected void validateCreate(TenantId tenantId, DeviceCredentials deviceCredentials) {
- DeviceCredentials existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
- if (existingCredentialsEntity != null) {
- throw new DataValidationException("Create of existent device credentials!");
+ if (!sqlDatabaseUsed) {
+ DeviceCredentials existingCredentialsEntity = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
+ if (existingCredentialsEntity != null) {
+ throw new DataValidationException("Create of existent device credentials!");
+ }
}
}
@@ -112,9 +129,11 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService {
if (existingCredentials == null) {
throw new DataValidationException("Unable to update non-existent device credentials!");
}
- DeviceCredentials sameCredentialsId = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
- if (sameCredentialsId != null && !sameCredentialsId.getUuidId().equals(deviceCredentials.getUuidId())) {
- throw new DataValidationException("Specified credentials are already registered!");
+ if (!sqlDatabaseUsed) {
+ DeviceCredentials sameCredentialsId = deviceCredentialsDao.findByCredentialsId(tenantId, deviceCredentials.getCredentialsId());
+ if (sameCredentialsId != null && !sameCredentialsId.getUuidId().equals(deviceCredentials.getUuidId())) {
+ throw new DataValidationException("Specified credentials are already registered!");
+ }
}
}
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 872e03a387..a26e275a96 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
@@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
+import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@@ -123,7 +124,21 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
public Device saveDevice(Device device) {
log.trace("Executing saveDevice [{}]", device);
deviceValidator.validate(device, Device::getTenantId);
- Device savedDevice = deviceDao.save(device.getTenantId(), device);
+ Device savedDevice;
+ if (!sqlDatabaseUsed) {
+ savedDevice = deviceDao.save(device.getTenantId(), device);
+ } else {
+ try {
+ savedDevice = 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")) {
+ 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()));
@@ -296,22 +311,26 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
@Override
protected void validateCreate(TenantId tenantId, Device device) {
- deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
- d -> {
- throw new DataValidationException("Device with such name already exists!");
- }
- );
+ if (!sqlDatabaseUsed) {
+ deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
+ d -> {
+ throw new DataValidationException("Device with such name already exists!");
+ }
+ );
+ }
}
@Override
protected void validateUpdate(TenantId tenantId, Device device) {
- deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
- d -> {
- if (!d.getUuidId().equals(device.getUuidId())) {
- throw new DataValidationException("Device with such name already exists!");
+ if (!sqlDatabaseUsed) {
+ deviceDao.findDeviceByTenantIdAndName(device.getTenantId().getId(), device.getName()).ifPresent(
+ d -> {
+ if (!d.getUuidId().equals(device.getUuidId())) {
+ throw new DataValidationException("Device with such name already exists!");
+ }
}
- }
- );
+ );
+ }
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
index 5280637020..2fce1fd758 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
@@ -16,20 +16,45 @@
package org.thingsboard.server.dao.entity;
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.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.relation.RelationService;
+import javax.annotation.PostConstruct;
+import java.util.Optional;
+
@Slf4j
public abstract class AbstractEntityService {
@Autowired
protected RelationService relationService;
+ @Value("${database.entities.type:sql}")
+ private String databaseType;
+
+ protected boolean sqlDatabaseUsed;
+
+ @PostConstruct
+ public void init() {
+ sqlDatabaseUsed = "sql".equalsIgnoreCase(databaseType);
+ }
+
protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) {
log.trace("Executing deleteEntityRelations [{}]", entityId);
relationService.deleteEntityRelations(tenantId, entityId);
}
+ protected Optional extractConstraintViolationException(Exception t) {
+ if (t instanceof ConstraintViolationException) {
+ return Optional.of ((ConstraintViolationException) t);
+ } else if (t.getCause() instanceof ConstraintViolationException) {
+ return Optional.of ((ConstraintViolationException) (t.getCause()));
+ } else {
+ return Optional.empty();
+ }
+ }
+
}
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 f065443489..c466c034fc 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
@@ -197,6 +197,7 @@ public class ModelConstants {
public static final String ASSET_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
public static final String ASSET_NAME_PROPERTY = "name";
public static final String ASSET_TYPE_PROPERTY = "type";
+ public static final String ASSET_LABEL_PROPERTY = "label";
public static final String ASSET_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_and_search_text";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
index a951b970f3..8313b44afe 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AssetEntity.java
@@ -37,6 +37,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@@ -64,6 +65,9 @@ public final class AssetEntity implements SearchTextEntity {
@Column(name = ASSET_NAME_PROPERTY)
private String name;
+ @Column(name = ASSET_LABEL_PROPERTY)
+ private String label;
+
@Column(name = SEARCH_TEXT_PROPERTY)
private String searchText;
@@ -86,6 +90,7 @@ public final class AssetEntity implements SearchTextEntity {
}
this.name = asset.getName();
this.type = asset.getType();
+ this.label = asset.getLabel();
this.additionalInfo = asset.getAdditionalInfo();
}
@@ -163,8 +168,9 @@ public final class AssetEntity implements SearchTextEntity {
}
asset.setName(name);
asset.setType(type);
+ asset.setLabel(label);
asset.setAdditionalInfo(additionalInfo);
return asset;
}
-}
\ No newline at end of file
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java
similarity index 85%
rename from dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java
rename to dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java
index 2773e5e4ab..4c8a2606d8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbsractTsKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java
@@ -16,14 +16,11 @@
package org.thingsboard.server.dao.model.sql;
import lombok.Data;
-import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
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.kv.TsKvEntry;
-import org.thingsboard.server.dao.model.ToData;
import javax.persistence.Column;
import javax.persistence.Id;
@@ -35,11 +32,10 @@ import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.LONG_VALUE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.STRING_VALUE_COLUMN;
-import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@Data
@MappedSuperclass
-public abstract class AbsractTsKvEntity implements ToData {
+public abstract class AbstractTsKvEntity {
protected static final String SUM = "SUM";
protected static final String AVG = "AVG";
@@ -50,10 +46,6 @@ public abstract class AbsractTsKvEntity implements ToData {
@Column(name = ENTITY_ID_COLUMN)
protected String entityId;
- @Id
- @Column(name = TS_COLUMN)
- protected Long ts;
-
@Id
@Column(name = KEY_COLUMN)
protected String key;
@@ -70,8 +62,7 @@ public abstract class AbsractTsKvEntity implements ToData {
@Column(name = DOUBLE_VALUE_COLUMN)
protected Double doubleValue;
- @Override
- public TsKvEntry toData() {
+ protected KvEntry getKvEntry() {
KvEntry kvEntry = null;
if (strValue != null) {
kvEntry = new StringDataEntry(key, strValue);
@@ -82,14 +73,14 @@ public abstract class AbsractTsKvEntity implements ToData {
} else if (booleanValue != null) {
kvEntry = new BooleanDataEntry(key, booleanValue);
}
- return new BasicTsKvEntry(ts, kvEntry);
+ return kvEntry;
}
public abstract boolean isNotEmpty();
protected static boolean isAllNull(Object... args) {
for (Object arg : args) {
- if(arg != null) {
+ if (arg != null) {
return false;
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
index 043a02b813..bfc8020a7b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AssetEntity.java
@@ -40,6 +40,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.ASSET_CUSTOMER_ID_
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_NAME_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TENANT_ID_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.ASSET_TYPE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.ASSET_LABEL_PROPERTY;
import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
@Data
@@ -61,6 +62,9 @@ public final class AssetEntity extends BaseSqlEntity implements SearchTex
@Column(name = ASSET_TYPE_PROPERTY)
private String type;
+ @Column(name = ASSET_LABEL_PROPERTY)
+ private String label;
+
@Column(name = SEARCH_TEXT_PROPERTY)
private String searchText;
@@ -84,6 +88,7 @@ public final class AssetEntity extends BaseSqlEntity implements SearchTex
}
this.name = asset.getName();
this.type = asset.getType();
+ this.label = asset.getLabel();
this.additionalInfo = asset.getAdditionalInfo();
}
@@ -113,8 +118,9 @@ public final class AssetEntity extends BaseSqlEntity implements SearchTex
}
asset.setName(name);
asset.setType(type);
+ asset.setLabel(label);
asset.setAdditionalInfo(additionalInfo);
return asset;
}
-}
\ No newline at end of file
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java
index fa212dd2f9..753e2c10fa 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/TimescaleTsKvEntity.java
@@ -18,9 +18,10 @@ package org.thingsboard.server.dao.model.sqlts.timescale;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
-import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity;
+import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import javax.persistence.Column;
import javax.persistence.ColumnResult;
@@ -35,6 +36,7 @@ import javax.persistence.SqlResultSetMappings;
import javax.persistence.Table;
import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_AVG;
import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_AVG_QUERY;
import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.FIND_COUNT;
@@ -113,12 +115,16 @@ import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.F
resultSetMapping = "timescaleCountMapping"
)
})
-public final class TimescaleTsKvEntity extends AbsractTsKvEntity implements ToData {
+public final class TimescaleTsKvEntity extends AbstractTsKvEntity implements ToData {
@Id
@Column(name = TENANT_ID_COLUMN)
private String tenantId;
+ @Id
+ @Column(name = TS_COLUMN)
+ protected Long ts;
+
public TimescaleTsKvEntity() { }
public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType) {
@@ -181,4 +187,9 @@ public final class TimescaleTsKvEntity extends AbsractTsKvEntity implements ToDa
public boolean isNotEmpty() {
return ts != null && (strValue != null || longValue != null || doubleValue != null || booleanValue != null);
}
+
+ @Override
+ public TsKvEntry toData() {
+ return new BasicTsKvEntry(ts, getKvEntry());
+ }
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java
index 4440a3dd0c..dab344cb44 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java
@@ -17,9 +17,10 @@ package org.thingsboard.server.dao.model.sqlts.ts;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ToData;
-import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity;
+import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -30,18 +31,23 @@ import javax.persistence.IdClass;
import javax.persistence.Table;
import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN;
+import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN;
@Data
@Entity
@Table(name = "ts_kv")
@IdClass(TsKvCompositeKey.class)
-public final class TsKvEntity extends AbsractTsKvEntity implements ToData {
+public final class TsKvEntity extends AbstractTsKvEntity implements ToData {
@Id
@Enumerated(EnumType.STRING)
@Column(name = ENTITY_TYPE_COLUMN)
private EntityType entityType;
+ @Id
+ @Column(name = TS_COLUMN)
+ protected Long ts;
+
public TsKvEntity() {
}
@@ -100,9 +106,13 @@ public final class TsKvEntity extends AbsractTsKvEntity implements ToData {
+public final class TsKvLatestEntity extends AbstractTsKvEntity implements ToData {
-
- //TODO: reafctor this and TsKvEntity to avoid code duplicates
@Id
@Enumerated(EnumType.STRING)
@Column(name = ENTITY_TYPE_COLUMN)
private EntityType entityType;
- @Id
- @Column(name = ENTITY_ID_COLUMN)
- private String entityId;
-
- @Id
- @Column(name = KEY_COLUMN)
- private String key;
-
@Column(name = TS_COLUMN)
private long ts;
- @Column(name = BOOLEAN_VALUE_COLUMN)
- private Boolean booleanValue;
-
- @Column(name = STRING_VALUE_COLUMN)
- private String strValue;
-
- @Column(name = LONG_VALUE_COLUMN)
- private Long longValue;
-
- @Column(name = DOUBLE_VALUE_COLUMN)
- private Double doubleValue;
-
@Override
public TsKvEntry toData() {
- KvEntry kvEntry = null;
- if (strValue != null) {
- kvEntry = new StringDataEntry(key, strValue);
- } else if (longValue != null) {
- kvEntry = new LongDataEntry(key, longValue);
- } else if (doubleValue != null) {
- kvEntry = new DoubleDataEntry(key, doubleValue);
- } else if (booleanValue != null) {
- kvEntry = new BooleanDataEntry(key, booleanValue);
- }
- return new BasicTsKvEntry(ts, kvEntry);
+ return new BasicTsKvEntry(ts, getKvEntry());
+ }
+
+ @Override
+ public boolean isNotEmpty() {
+ return strValue != null || longValue != null || doubleValue != null || booleanValue != null;
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java
index 12c2ee85f5..7870135592 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java
@@ -56,19 +56,32 @@ public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor<
@Value("${cassandra.query.poll_ms:50}") long pollMs,
@Value("${cassandra.query.tenant_rate_limits.enabled}") boolean tenantRateLimitsEnabled,
@Value("${cassandra.query.tenant_rate_limits.configuration}") String tenantRateLimitsConfiguration,
- @Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames) {
- super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration);
+ @Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames,
+ @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq) {
+ super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration, printQueriesFreq);
this.printTenantNames = printTenantNames;
}
@Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
public void printStats() {
- log.info("Permits queueSize [{}] totalAdded [{}] totalLaunched [{}] totalReleased [{}] totalFailed [{}] totalExpired [{}] totalRejected [{}] " +
- "totalRateLimited [{}] totalRateLimitedTenants [{}] currBuffer [{}] ",
- getQueueSize(),
- totalAdded.getAndSet(0), totalLaunched.getAndSet(0), totalReleased.getAndSet(0),
- totalFailed.getAndSet(0), totalExpired.getAndSet(0), totalRejected.getAndSet(0),
- totalRateLimited.getAndSet(0), rateLimitedTenants.size(), concurrencyLevel.get());
+ int queueSize = getQueueSize();
+ int totalAddedValue = totalAdded.getAndSet(0);
+ int totalLaunchedValue = totalLaunched.getAndSet(0);
+ int totalReleasedValue = totalReleased.getAndSet(0);
+ int totalFailedValue = totalFailed.getAndSet(0);
+ int totalExpiredValue = totalExpired.getAndSet(0);
+ int totalRejectedValue = totalRejected.getAndSet(0);
+ int totalRateLimitedValue = totalRateLimited.getAndSet(0);
+ int rateLimitedTenantsValue = rateLimitedTenants.size();
+ int concurrencyLevelValue = concurrencyLevel.get();
+ if (queueSize > 0 || totalAddedValue > 0 || totalLaunchedValue > 0 || totalReleasedValue > 0 ||
+ totalFailedValue > 0 || totalExpiredValue > 0 || totalRejectedValue > 0 || totalRateLimitedValue > 0 || rateLimitedTenantsValue > 0
+ || concurrencyLevelValue > 0) {
+ log.info("Permits queueSize [{}] totalAdded [{}] totalLaunched [{}] totalReleased [{}] totalFailed [{}] totalExpired [{}] totalRejected [{}] " +
+ "totalRateLimited [{}] totalRateLimitedTenants [{}] currBuffer [{}] ",
+ queueSize, totalAddedValue, totalLaunchedValue, totalReleasedValue,
+ totalFailedValue, totalExpiredValue, totalRejectedValue, totalRateLimitedValue, rateLimitedTenantsValue, concurrencyLevelValue);
+ }
rateLimitedTenants.forEach(((tenantId, counter) -> {
if (printTenantNames) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
index 16368dff71..16723c024d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
@@ -17,16 +17,14 @@ package org.thingsboard.server.dao.sql;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
+import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
public abstract class JpaAbstractDaoListeningExecutorService {
- protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
+ @Autowired
+ protected JpaExecutorService service;
- @PreDestroy
- void onDestroy() {
- service.shutdown();
- }
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java
new file mode 100644
index 0000000000..998b8bb444
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaExecutorService.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2019 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;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.common.util.AbstractListeningExecutor;
+import org.thingsboard.server.dao.util.SqlDao;
+
+@Component
+@SqlDao
+public class JpaExecutorService extends AbstractListeningExecutor {
+
+ @Value("${spring.datasource.hikari.maximumPoolSize}")
+ private int poolSize;
+
+ @Override
+ protected int getThreadPollSize() {
+ return poolSize;
+ }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java
new file mode 100644
index 0000000000..da20ff8c21
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ScheduledLogExecutorComponent.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2019 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;
+
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class ScheduledLogExecutorComponent {
+
+ private ScheduledExecutorService schedulerLogExecutor;
+
+ @PostConstruct
+ public void init() {
+ schedulerLogExecutor = Executors.newSingleThreadScheduledExecutor();
+ }
+
+ @PreDestroy
+ public void stop() {
+ if (schedulerLogExecutor != null) {
+ schedulerLogExecutor.shutdownNow();
+ }
+ }
+
+ public void scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
+ schedulerLogExecutor.scheduleAtFixedRate(command, initialDelay, period, unit);
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java
new file mode 100644
index 0000000000..da0a9cd70d
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueue.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016-2019 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;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class TbSqlBlockingQueue implements TbSqlQueue {
+
+ private final BlockingQueue> queue = new LinkedBlockingQueue<>();
+ private final AtomicInteger addedCount = new AtomicInteger();
+ private final AtomicInteger savedCount = new AtomicInteger();
+ private final AtomicInteger failedCount = new AtomicInteger();
+ private final TbSqlBlockingQueueParams params;
+
+ private ExecutorService executor;
+ private ScheduledLogExecutorComponent logExecutor;
+
+ public TbSqlBlockingQueue(TbSqlBlockingQueueParams params) {
+ this.params = params;
+ }
+
+ @Override
+ public void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction) {
+ this.logExecutor = logExecutor;
+ executor = Executors.newSingleThreadExecutor();
+ executor.submit(() -> {
+ String logName = params.getLogName();
+ int batchSize = params.getBatchSize();
+ long maxDelay = params.getMaxDelay();
+ List> entities = new ArrayList<>(batchSize);
+ while (!Thread.interrupted()) {
+ try {
+ long currentTs = System.currentTimeMillis();
+ TbSqlQueueElement attr = queue.poll(maxDelay, TimeUnit.MILLISECONDS);
+ if (attr == null) {
+ continue;
+ } else {
+ entities.add(attr);
+ }
+ queue.drainTo(entities, batchSize - 1);
+ boolean fullPack = entities.size() == batchSize;
+ log.debug("[{}] Going to save {} entities", logName, entities.size());
+ saveFunction.accept(entities.stream().map(TbSqlQueueElement::getEntity).collect(Collectors.toList()));
+ entities.forEach(v -> v.getFuture().set(null));
+ savedCount.addAndGet(entities.size());
+ if (!fullPack) {
+ long remainingDelay = maxDelay - (System.currentTimeMillis() - currentTs);
+ if (remainingDelay > 0) {
+ Thread.sleep(remainingDelay);
+ }
+ }
+ } catch (Exception e) {
+ failedCount.addAndGet(entities.size());
+ entities.forEach(entityFutureWrapper -> entityFutureWrapper.getFuture().setException(e));
+ if (e instanceof InterruptedException) {
+ log.info("[{}] Queue polling was interrupted", logName);
+ break;
+ } else {
+ log.error("[{}] Failed to save {} entities", logName, entities.size(), e);
+ }
+ } finally {
+ entities.clear();
+ }
+ }
+ });
+
+ logExecutor.scheduleAtFixedRate(() -> {
+ if (queue.size() > 0 || addedCount.get() > 0 || savedCount.get() > 0 || failedCount.get() > 0) {
+ log.info("[{}] queueSize [{}] totalAdded [{}] totalSaved [{}] totalFailed [{}]",
+ params.getLogName(), queue.size(), addedCount.getAndSet(0), savedCount.getAndSet(0), failedCount.getAndSet(0));
+ }
+ }, params.getStatsPrintIntervalMs(), params.getStatsPrintIntervalMs(), TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void destroy() {
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ @Override
+ public ListenableFuture add(E element) {
+ SettableFuture future = SettableFuture.create();
+ queue.add(new TbSqlQueueElement<>(future, element));
+ addedCount.incrementAndGet();
+ return future;
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java
new file mode 100644
index 0000000000..9afe19a61a
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlBlockingQueueParams.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2019 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;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Data
+@Builder
+public class TbSqlBlockingQueueParams {
+
+ private final String logName;
+ private final int batchSize;
+ private final long maxDelay;
+ private final long statsPrintIntervalMs;
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java
new file mode 100644
index 0000000000..27c6bc9509
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueue.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2019 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;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public interface TbSqlQueue {
+
+ void init(ScheduledLogExecutorComponent logExecutor, Consumer> saveFunction);
+
+ void destroy();
+
+ ListenableFuture add(E element);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java
new file mode 100644
index 0000000000..7c95d768e7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/TbSqlQueueElement.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2019 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;
+
+import com.google.common.util.concurrent.SettableFuture;
+import lombok.Getter;
+
+public final class TbSqlQueueElement {
+ @Getter
+ private final SettableFuture future;
+ @Getter
+ private final E entity;
+
+ public TbSqlQueueElement(SettableFuture future, E entity) {
+ this.future = future;
+ this.entity = entity;
+ }
+}
+
+
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 a3d5b45a39..756ebbf070 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
@@ -31,11 +31,8 @@ import java.util.List;
@SqlDao
public interface AlarmRepository extends CrudRepository {
- @Query("SELECT a FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.originatorId = :originatorId " +
- "AND a.originatorType = :entityType AND a.type = :alarmType ORDER BY a.type ASC, a.id DESC")
- List findLatestByOriginatorAndType(@Param("tenantId") String tenantId,
- @Param("originatorId") String originatorId,
- @Param("entityType") EntityType entityType,
+ @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType ORDER BY startTs DESC")
+ List findLatestByOriginatorAndType(@Param("originatorId") String originatorId,
@Param("alarmType") String alarmType,
Pageable pageable);
}
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 4762c5c4ec..8aa9a81094 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
@@ -77,11 +77,9 @@ public class JpaAlarmDao extends JpaAbstractDao implements A
public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return service.submit(() -> {
List latest = alarmRepository.findLatestByOriginatorAndType(
- UUIDConverter.fromTimeUUID(tenantId.getId()),
UUIDConverter.fromTimeUUID(originator.getId()),
- originator.getEntityType(),
type,
- new PageRequest(0, 1));
+ PageRequest.of(0, 1));
return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0));
});
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java
index e50a60aed9..0a537cbe01 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvInsertRepository.java
@@ -15,23 +15,59 @@
*/
package org.thingsboard.server.dao.sql.attributes;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
+import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.TransactionCallbackWithoutResult;
+import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
import org.thingsboard.server.dao.util.SqlDao;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
@SqlDao
@Repository
+@Slf4j
public abstract class AttributeKvInsertRepository {
+ private static final ThreadLocal PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE)));
+ private static final String EMPTY_STR = "";
+
+ private static final String BATCH_UPDATE = "UPDATE attribute_kv SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, last_update_ts = ? " +
+ "WHERE entity_type = ? and entity_id = ? and attribute_type =? and attribute_key = ?;";
+
+ private static final String INSERT_OR_UPDATE =
+ "INSERT INTO attribute_kv (entity_type, entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, last_update_ts) " +
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) " +
+ "ON CONFLICT (entity_type, entity_id, attribute_type, attribute_key) " +
+ "DO UPDATE SET str_v = ?, long_v = ?, dbl_v = ?, bool_v = ?, last_update_ts = ?;";
+
protected static final String BOOL_V = "bool_v";
protected static final String STR_V = "str_v";
protected static final String LONG_V = "long_v";
protected static final String DBL_V = "dbl_v";
+ @Autowired
+ protected JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ private TransactionTemplate transactionTemplate;
+
+ @Value("${sql.remove_null_chars}")
+ private boolean removeNullChars;
+
@PersistenceContext
protected EntityManager entityManager;
@@ -71,7 +107,7 @@ public abstract class AttributeKvInsertRepository {
.setParameter("entity_id", entity.getId().getEntityId())
.setParameter("attribute_type", entity.getId().getAttributeType())
.setParameter("attribute_key", entity.getId().getAttributeKey())
- .setParameter("str_v", entity.getStrValue())
+ .setParameter("str_v", replaceNullChars(entity.getStrValue()))
.setParameter("last_update_ts", entity.getLastUpdateTs())
.executeUpdate();
}
@@ -99,4 +135,112 @@ public abstract class AttributeKvInsertRepository {
.setParameter("last_update_ts", entity.getLastUpdateTs())
.executeUpdate();
}
+
+ protected void saveOrUpdate(List entities) {
+ transactionTemplate.execute(new TransactionCallbackWithoutResult() {
+ @Override
+ protected void doInTransactionWithoutResult(TransactionStatus status) {
+ int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setString(1, replaceNullChars(entities.get(i).getStrValue()));
+
+ if (entities.get(i).getLongValue() != null) {
+ ps.setLong(2, entities.get(i).getLongValue());
+ } else {
+ ps.setNull(2, Types.BIGINT);
+ }
+
+ if (entities.get(i).getDoubleValue() != null) {
+ ps.setDouble(3, entities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(3, Types.DOUBLE);
+ }
+
+ if (entities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(4, entities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(4, Types.BOOLEAN);
+ }
+
+ ps.setLong(5, entities.get(i).getLastUpdateTs());
+ ps.setString(6, entities.get(i).getId().getEntityType().name());
+ ps.setString(7, entities.get(i).getId().getEntityId());
+ ps.setString(8, entities.get(i).getId().getAttributeType());
+ ps.setString(9, entities.get(i).getId().getAttributeKey());
+ }
+
+ @Override
+ public int getBatchSize() {
+ return entities.size();
+ }
+ });
+
+ int updatedCount = 0;
+ for (int i = 0; i < result.length; i++) {
+ if (result[i] == 0) {
+ updatedCount++;
+ }
+ }
+
+ List insertEntities = new ArrayList<>(updatedCount);
+ for (int i = 0; i < result.length; i++) {
+ if (result[i] == 0) {
+ insertEntities.add(entities.get(i));
+ }
+ }
+
+ jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setString(1, insertEntities.get(i).getId().getEntityType().name());
+ ps.setString(2, insertEntities.get(i).getId().getEntityId());
+ ps.setString(3, insertEntities.get(i).getId().getAttributeType());
+ ps.setString(4, insertEntities.get(i).getId().getAttributeKey());
+ ps.setString(5, replaceNullChars(insertEntities.get(i).getStrValue()));
+ ps.setString(10, replaceNullChars(insertEntities.get(i).getStrValue()));
+
+ if (insertEntities.get(i).getLongValue() != null) {
+ ps.setLong(6, insertEntities.get(i).getLongValue());
+ ps.setLong(11, insertEntities.get(i).getLongValue());
+ } else {
+ ps.setNull(6, Types.BIGINT);
+ ps.setNull(11, Types.BIGINT);
+ }
+
+ if (insertEntities.get(i).getDoubleValue() != null) {
+ ps.setDouble(7, insertEntities.get(i).getDoubleValue());
+ ps.setDouble(12, insertEntities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(7, Types.DOUBLE);
+ ps.setNull(12, Types.DOUBLE);
+ }
+
+ if (insertEntities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(8, insertEntities.get(i).getBooleanValue());
+ ps.setBoolean(13, insertEntities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(8, Types.BOOLEAN);
+ ps.setNull(13, Types.BOOLEAN);
+ }
+
+ ps.setLong(9, insertEntities.get(i).getLastUpdateTs());
+ ps.setLong(14, insertEntities.get(i).getLastUpdateTs());
+ }
+
+ @Override
+ public int getBatchSize() {
+ return insertEntities.size();
+ }
+ });
+ }
+ });
+ }
+
+ private String replaceNullChars(String strValue) {
+ if (removeNullChars && strValue != null) {
+ return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR);
+ }
+ return strValue;
+ }
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/HsqlAttributesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/HsqlAttributesInsertRepository.java
index f2343cd7fe..425832f344 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/HsqlAttributesInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/HsqlAttributesInsertRepository.java
@@ -21,6 +21,9 @@ import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
import org.thingsboard.server.dao.util.HsqlDao;
import org.thingsboard.server.dao.util.SqlDao;
+import java.sql.Types;
+import java.util.List;
+
@SqlDao
@HsqlDao
@Repository
@@ -37,6 +40,17 @@ public class HsqlAttributesInsertRepository extends AttributeKvInsertRepository
private static final String INSERT_LONG_STATEMENT = getInsertOrUpdateString(LONG_V, ON_LONG_VALUE_UPDATE_SET_NULLS);
private static final String INSERT_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE =
+ "MERGE INTO attribute_kv USING(VALUES ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
+ "A (entity_type, entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, last_update_ts) " +
+ "ON (attribute_kv.entity_type=A.entity_type " +
+ "AND attribute_kv.entity_id=A.entity_id " +
+ "AND attribute_kv.attribute_type=A.attribute_type " +
+ "AND attribute_kv.attribute_key=A.attribute_key) " +
+ "WHEN MATCHED THEN UPDATE SET attribute_kv.str_v = A.str_v, attribute_kv.long_v = A.long_v, attribute_kv.dbl_v = A.dbl_v, attribute_kv.bool_v = A.bool_v, attribute_kv.last_update_ts = A.last_update_ts " +
+ "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, attribute_type, attribute_key, str_v, long_v, dbl_v, bool_v, last_update_ts) " +
+ "VALUES (A.entity_type, A.entity_id, A.attribute_type, A.attribute_key, A.str_v, A.long_v, A.dbl_v, A.bool_v, A.last_update_ts)";
+
@Override
public void saveOrUpdate(AttributeKvEntity entity) {
processSaveOrUpdate(entity, INSERT_BOOL_STATEMENT, INSERT_STR_STATEMENT, INSERT_LONG_STATEMENT, INSERT_DBL_STATEMENT);
@@ -45,4 +59,38 @@ public class HsqlAttributesInsertRepository extends AttributeKvInsertRepository
private static String getInsertOrUpdateString(String value, String nullValues) {
return "MERGE INTO attribute_kv USING(VALUES :entity_type, :entity_id, :attribute_type, :attribute_key, :" + value + ", :last_update_ts) A (entity_type, entity_id, attribute_type, attribute_key, " + value + ", last_update_ts) ON (attribute_kv.entity_type=A.entity_type AND attribute_kv.entity_id=A.entity_id AND attribute_kv.attribute_type=A.attribute_type AND attribute_kv.attribute_key=A.attribute_key) WHEN MATCHED THEN UPDATE SET attribute_kv." + value + " = A." + value + ", attribute_kv.last_update_ts = A.last_update_ts," + nullValues + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, attribute_type, attribute_key, " + value + ", last_update_ts) VALUES (A.entity_type, A.entity_id, A.attribute_type, A.attribute_key, A." + value + ", A.last_update_ts)";
}
+
+
+ @Override
+ protected void saveOrUpdate(List entities) {
+ entities.forEach(entity -> {
+ jdbcTemplate.update(INSERT_OR_UPDATE, ps -> {
+ ps.setString(1, entity.getId().getEntityType().name());
+ ps.setString(2, entity.getId().getEntityId());
+ ps.setString(3, entity.getId().getAttributeType());
+ ps.setString(4, entity.getId().getAttributeKey());
+ ps.setString(5, entity.getStrValue());
+
+ if (entity.getLongValue() != null) {
+ ps.setLong(6, entity.getLongValue());
+ } else {
+ ps.setNull(6, Types.BIGINT);
+ }
+
+ if (entity.getDoubleValue() != null) {
+ ps.setDouble(7, entity.getDoubleValue());
+ } else {
+ ps.setNull(7, Types.DOUBLE);
+ }
+
+ if (entity.getBooleanValue() != null) {
+ ps.setBoolean(8, entity.getBooleanValue());
+ } else {
+ ps.setNull(8, Types.BOOLEAN);
+ }
+
+ ps.setLong(9, entity.getLastUpdateTs());
+ });
+ });
+ }
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
index b8ad6f8271..2b8c7e5ab2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
@@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.common.data.id.EntityId;
@@ -30,8 +31,13 @@ import org.thingsboard.server.dao.attributes.AttributesDao;
import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey;
import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
+import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
+import org.thingsboard.server.dao.sql.TbSqlBlockingQueue;
+import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.util.SqlDao;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -44,12 +50,45 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@SqlDao
public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService implements AttributesDao {
+ @Autowired
+ ScheduledLogExecutorComponent logExecutor;
+
@Autowired
private AttributeKvRepository attributeKvRepository;
@Autowired
private AttributeKvInsertRepository attributeKvInsertRepository;
+ @Value("${sql.attributes.batch_size:1000}")
+ private int batchSize;
+
+ @Value("${sql.attributes.batch_max_delay:100}")
+ private long maxDelay;
+
+ @Value("${sql.attributes.stats_print_interval_ms:1000}")
+ private long statsPrintIntervalMs;
+
+ private TbSqlBlockingQueue queue;
+
+ @PostConstruct
+ private void init() {
+ TbSqlBlockingQueueParams params = TbSqlBlockingQueueParams.builder()
+ .logName("Attributes")
+ .batchSize(batchSize)
+ .maxDelay(maxDelay)
+ .statsPrintIntervalMs(statsPrintIntervalMs)
+ .build();
+ queue = new TbSqlBlockingQueue<>(params);
+ queue.init(logExecutor, v -> attributeKvInsertRepository.saveOrUpdate(v));
+ }
+
+ @PreDestroy
+ private void destroy() {
+ if (queue != null) {
+ queue.destroy();
+ }
+ }
+
@Override
public ListenableFuture> find(TenantId tenantId, EntityId entityId, String attributeType, String attributeKey) {
AttributeKvCompositeKey compositeKey =
@@ -89,12 +128,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
entity.setDoubleValue(attribute.getDoubleValue().orElse(null));
entity.setLongValue(attribute.getLongValue().orElse(null));
entity.setBooleanValue(attribute.getBooleanValue().orElse(null));
- return service.submit(() -> {
- attributeKvInsertRepository.saveOrUpdate(entity);
- return null;
- });
+ return addToQueue(entity);
}
+ private ListenableFuture addToQueue(AttributeKvEntity entity) {
+ return queue.add(entity);
+ }
@Override
public ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List keys) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
index a2c18c541b..9b6638b6cf 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java
@@ -53,8 +53,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.ID_PROPERTY;
@SqlDao
public class JpaAuditLogDao extends JpaAbstractDao implements AuditLogDao {
- private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
-
@Autowired
private AuditLogRepository auditLogRepository;
@@ -68,14 +66,9 @@ public class JpaAuditLogDao extends JpaAbstractDao imp
return auditLogRepository;
}
- @PreDestroy
- void onDestroy() {
- insertService.shutdown();
- }
-
@Override
public ListenableFuture saveByTenantId(AuditLog auditLog) {
- return insertService.submit(() -> {
+ return service.submit(() -> {
save(auditLog.getTenantId(), auditLog);
return null;
});
@@ -83,22 +76,22 @@ public class JpaAuditLogDao extends JpaAbstractDao imp
@Override
public ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog) {
- return insertService.submit(() -> null);
+ return service.submit(() -> null);
}
@Override
public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) {
- return insertService.submit(() -> null);
+ return service.submit(() -> null);
}
@Override
public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) {
- return insertService.submit(() -> null);
+ return service.submit(() -> null);
}
@Override
public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) {
- return insertService.submit(() -> null);
+ return service.submit(() -> null);
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
new file mode 100644
index 0000000000..7bb98a9163
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016-2019 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.component;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.exception.ConstraintViolationException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.persistence.Query;
+
+@Slf4j
+public abstract class AbstractComponentDescriptorInsertRepository implements ComponentDescriptorInsertRepository {
+
+ @PersistenceContext
+ protected EntityManager entityManager;
+
+ @Autowired
+ protected PlatformTransactionManager transactionManager;
+
+ protected ComponentDescriptorEntity saveAndGet(ComponentDescriptorEntity entity, String insertOrUpdateOnPrimaryKeyConflict, String insertOrUpdateOnUniqueKeyConflict) {
+ ComponentDescriptorEntity componentDescriptorEntity = null;
+ TransactionStatus insertTransaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRED);
+ try {
+ componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnPrimaryKeyConflict);
+ transactionManager.commit(insertTransaction);
+ } catch (Throwable throwable) {
+ transactionManager.rollback(insertTransaction);
+ if (throwable.getCause() instanceof ConstraintViolationException) {
+ log.trace("Insert request leaded in a violation of a defined integrity constraint {} for Component Descriptor with id {}, name {} and entityType {}", throwable.getMessage(), entity.getId(), entity.getName(), entity.getType());
+ TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+ try {
+ componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
+ } catch (Throwable th) {
+ log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getId(), entity.getName(), entity.getType());
+ transactionManager.rollback(transaction);
+ }
+ transactionManager.commit(transaction);
+ } else {
+ log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getId(), entity.getName(), entity.getType());
+ }
+ }
+ return componentDescriptorEntity;
+ }
+
+ @Modifying
+ protected abstract ComponentDescriptorEntity doProcessSaveOrUpdate(ComponentDescriptorEntity entity, String query);
+
+ protected Query getQuery(ComponentDescriptorEntity entity, String query) {
+ return entityManager.createNativeQuery(query, ComponentDescriptorEntity.class)
+ .setParameter("id", UUIDConverter.fromTimeUUID(entity.getId()))
+ .setParameter("actions", entity.getActions())
+ .setParameter("clazz", entity.getClazz())
+ .setParameter("configuration_descriptor", entity.getConfigurationDescriptor().toString())
+ .setParameter("name", entity.getName())
+ .setParameter("scope", entity.getScope().name())
+ .setParameter("search_text", entity.getSearchText())
+ .setParameter("type", entity.getType().name());
+ }
+
+ private ComponentDescriptorEntity processSaveOrUpdate(ComponentDescriptorEntity entity, String query) {
+ return doProcessSaveOrUpdate(entity, query);
+ }
+
+ private TransactionStatus getTransactionStatus(int propagationRequired) {
+ DefaultTransactionDefinition insertDefinition = new DefaultTransactionDefinition();
+ insertDefinition.setPropagationBehavior(propagationRequired);
+ return transactionManager.getTransaction(insertDefinition);
+ }
+}
diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaEnricher.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorInsertRepository.java
similarity index 71%
rename from common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaEnricher.java
rename to dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorInsertRepository.java
index b9226bbd38..dd24e38c11 100644
--- a/common/queue/src/main/java/org/thingsboard/server/kafka/TbKafkaEnricher.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorInsertRepository.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.thingsboard.server.kafka;
+package org.thingsboard.server.dao.sql.component;
-import java.util.UUID;
+import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity;
-public interface TbKafkaEnricher {
+public interface ComponentDescriptorInsertRepository {
- T enrich(T value, String responseTopic, UUID requestId);
+ ComponentDescriptorEntity saveOrUpdate(ComponentDescriptorEntity entity);
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java
new file mode 100644
index 0000000000..4ceb6cecc0
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/HsqlComponentDescriptorInsertRepository.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2019 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.component;
+
+import org.springframework.stereotype.Repository;
+import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.dao.model.sql.ComponentDescriptorEntity;
+import org.thingsboard.server.dao.util.HsqlDao;
+import org.thingsboard.server.dao.util.SqlDao;
+
+@SqlDao
+@HsqlDao
+@Repository
+public class HsqlComponentDescriptorInsertRepository extends AbstractComponentDescriptorInsertRepository {
+
+ private static final String P_KEY_CONFLICT_STATEMENT = "(component_descriptor.id=I.id)";
+ private static final String UNQ_KEY_CONFLICT_STATEMENT = "(component_descriptor.clazz=I.clazz)";
+
+ private static final String INSERT_OR_UPDATE_ON_P_KEY_CONFLICT = getInsertString(P_KEY_CONFLICT_STATEMENT);
+ private static final String INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT = getInsertString(UNQ_KEY_CONFLICT_STATEMENT);
+
+ @Override
+ public ComponentDescriptorEntity saveOrUpdate(ComponentDescriptorEntity entity) {
+ return saveAndGet(entity, INSERT_OR_UPDATE_ON_P_KEY_CONFLICT, INSERT_OR_UPDATE_ON_UNQ_KEY_CONFLICT);
+ }
+
+ @Override
+ protected ComponentDescriptorEntity doProcessSaveOrUpdate(ComponentDescriptorEntity entity, String query) {
+ getQuery(entity, query).executeUpdate();
+ return entityManager.find(ComponentDescriptorEntity.class, UUIDConverter.fromTimeUUID(entity.getId()));
+ }
+
+ private static String getInsertString(String conflictStatement) {
+ return "MERGE INTO component_descriptor USING (VALUES :id, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) I (id, actions, clazz, configuration_descriptor, name, scope, search_text, type) ON " + conflictStatement + " WHEN MATCHED THEN UPDATE SET component_descriptor.id = I.id, component_descriptor.actions = I.actions, component_descriptor.clazz = I.clazz, component_descriptor.configuration_descriptor = I.configuration_descriptor, component_descriptor.name = I.name, component_descriptor.scope = I.scope, component_descriptor.search_text = I.search_text, component_descriptor.type = I.type" +
+ " WHEN NOT MATCHED THEN INSERT (id, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (I.id, I.actions, I.clazz, I.configuration_descriptor, I.name, I.scope, I.search_text, I.type)";
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java
index c66ea9c5cc..0b764eafd4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/JpaBaseComponentDescriptorDao.java
@@ -51,6 +51,9 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao getEntityClass() {
return ComponentDescriptorEntity.class;
@@ -67,7 +70,9 @@ public class JpaBaseComponentDescriptorDao extends JpaAbstractSearchTextDao {
@Param("idOffset") String idOffset,
Pageable pageable);
+ @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
+ "AND d.id > :idOffset ORDER BY d.id")
+ List findByTenantId(@Param("tenantId") String tenantId,
+ @Param("idOffset") String idOffset,
+ Pageable pageable);
+
@Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " +
"AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%')) " +
"AND d.id > :idOffset ORDER BY d.id")
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
index be0b4b89df..0355ca95f2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
@@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
@@ -65,12 +66,20 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao
@Override
public List findDevicesByTenantId(UUID tenantId, TextPageLink pageLink) {
- return DaoUtil.convertDataList(
- deviceRepository.findByTenantId(
- fromTimeUUID(tenantId),
- Objects.toString(pageLink.getTextSearch(), ""),
- pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
- new PageRequest(0, pageLink.getLimit())));
+ if (StringUtils.isEmpty(pageLink.getTextSearch())) {
+ return DaoUtil.convertDataList(
+ deviceRepository.findByTenantId(
+ fromTimeUUID(tenantId),
+ pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
+ new PageRequest(0, pageLink.getLimit())));
+ } else {
+ return DaoUtil.convertDataList(
+ deviceRepository.findByTenantId(
+ fromTimeUUID(tenantId),
+ Objects.toString(pageLink.getTextSearch(), ""),
+ pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()),
+ new PageRequest(0, pageLink.getLimit())));
+ }
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java
new file mode 100644
index 0000000000..60201fcf82
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright © 2016-2019 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.event;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.exception.ConstraintViolationException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.dao.model.sql.EventEntity;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.persistence.Query;
+
+@Slf4j
+public abstract class AbstractEventInsertRepository implements EventInsertRepository {
+
+ @PersistenceContext
+ protected EntityManager entityManager;
+
+ @Autowired
+ protected PlatformTransactionManager transactionManager;
+
+ protected EventEntity saveAndGet(EventEntity entity, String insertOrUpdateOnPrimaryKeyConflict, String insertOrUpdateOnUniqueKeyConflict) {
+ EventEntity eventEntity = null;
+ TransactionStatus insertTransaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRED);
+ try {
+ eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnPrimaryKeyConflict);
+ transactionManager.commit(insertTransaction);
+ } catch (Throwable throwable) {
+ transactionManager.rollback(insertTransaction);
+ if (throwable.getCause() instanceof ConstraintViolationException) {
+ log.trace("Insert request leaded in a violation of a defined integrity constraint {} for Entity with entityId {} and entityType {}", throwable.getMessage(), entity.getEventUid(), entity.getEventType());
+ TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+ try {
+ eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
+ } catch (Throwable th) {
+ log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
+ transactionManager.rollback(transaction);
+ }
+ transactionManager.commit(transaction);
+ } else {
+ log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
+ }
+ }
+ return eventEntity;
+ }
+
+ @Modifying
+ protected abstract EventEntity doProcessSaveOrUpdate(EventEntity entity, String query);
+
+ protected Query getQuery(EventEntity entity, String query) {
+ return entityManager.createNativeQuery(query, EventEntity.class)
+ .setParameter("id", UUIDConverter.fromTimeUUID(entity.getId()))
+ .setParameter("body", entity.getBody().toString())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("event_type", entity.getEventType())
+ .setParameter("event_uid", entity.getEventUid())
+ .setParameter("tenant_id", entity.getTenantId());
+ }
+
+ private EventEntity processSaveOrUpdate(EventEntity entity, String query) {
+ return doProcessSaveOrUpdate(entity, query);
+ }
+
+ private TransactionStatus getTransactionStatus(int propagationRequired) {
+ DefaultTransactionDefinition insertDefinition = new DefaultTransactionDefinition();
+ insertDefinition.setPropagationBehavior(propagationRequired);
+ return transactionManager.getTransaction(insertDefinition);
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java
index 051453cf63..491c1fb8b6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventInsertRepository.java
@@ -15,82 +15,10 @@
*/
package org.thingsboard.server.dao.sql.event;
-import lombok.extern.slf4j.Slf4j;
-import org.hibernate.exception.ConstraintViolationException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.jpa.repository.Modifying;
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.TransactionDefinition;
-import org.springframework.transaction.TransactionStatus;
-import org.springframework.transaction.support.DefaultTransactionDefinition;
-import org.thingsboard.server.common.data.UUIDConverter;
import org.thingsboard.server.dao.model.sql.EventEntity;
-import org.thingsboard.server.dao.util.SqlDao;
-import javax.persistence.EntityManager;
-import javax.persistence.PersistenceContext;
-import javax.persistence.Query;
+public interface EventInsertRepository {
-@Slf4j
-@SqlDao
-@Repository
-public abstract class EventInsertRepository {
+ EventEntity saveOrUpdate(EventEntity entity);
- @PersistenceContext
- protected EntityManager entityManager;
-
- @Autowired
- protected PlatformTransactionManager transactionManager;
-
- public abstract EventEntity saveOrUpdate(EventEntity entity);
-
- protected EventEntity saveAndGet(EventEntity entity, String insertOrUpdateOnPrimaryKeyConflict, String insertOrUpdateOnUniqueKeyConflict) {
- EventEntity eventEntity = null;
- TransactionStatus insertTransaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRED);
- try {
- eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnPrimaryKeyConflict);
- transactionManager.commit(insertTransaction);
- } catch (Throwable throwable) {
- transactionManager.rollback(insertTransaction);
- if (throwable.getCause() instanceof ConstraintViolationException) {
- log.trace("Insert request leaded in a violation of a defined integrity constraint {} for Entity with entityId {} and entityType {}", throwable.getMessage(), entity.getEventUid(), entity.getEventType());
- TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
- try {
- eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
- } catch (Throwable th) {
- log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
- transactionManager.rollback(transaction);
- }
- transactionManager.commit(transaction);
- } else {
- log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
- }
- }
- return eventEntity;
- }
-
- @Modifying
- protected abstract EventEntity doProcessSaveOrUpdate(EventEntity entity, String query);
-
- protected Query getQuery(EventEntity entity, String query) {
- return entityManager.createNativeQuery(query, EventEntity.class)
- .setParameter("id", UUIDConverter.fromTimeUUID(entity.getId()))
- .setParameter("body", entity.getBody().toString())
- .setParameter("entity_id", entity.getEntityId())
- .setParameter("entity_type", entity.getEntityType().name())
- .setParameter("event_type", entity.getEventType())
- .setParameter("event_uid", entity.getEventUid())
- .setParameter("tenant_id", entity.getTenantId());
- }
-
- private EventEntity processSaveOrUpdate(EventEntity entity, String query) {
- return doProcessSaveOrUpdate(entity, query);
- }
-
- private TransactionStatus getTransactionStatus(int propagationRequired) {
- DefaultTransactionDefinition insertDefinition = new DefaultTransactionDefinition();
- insertDefinition.setPropagationBehavior(propagationRequired);
- return transactionManager.getTransaction(insertDefinition);
- }
-}
\ No newline at end of file
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java
index e34cc7b70c..54b3da2efc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventInsertRepository.java
@@ -24,7 +24,7 @@ import org.thingsboard.server.dao.util.SqlDao;
@SqlDao
@HsqlDao
@Repository
-public class HsqlEventInsertRepository extends EventInsertRepository {
+public class HsqlEventInsertRepository extends AbstractEventInsertRepository {
private static final String P_KEY_CONFLICT_STATEMENT = "(event.id=I.id)";
private static final String UNQ_KEY_CONFLICT_STATEMENT = "(event.tenant_id=I.tenant_id AND event.entity_type=I.entity_type AND event.entity_id=I.entity_id AND event.event_type=I.event_type AND event.event_uid=I.event_uid)";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java
index e4fd1ed37c..7e07e48983 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventInsertRepository.java
@@ -25,7 +25,7 @@ import org.thingsboard.server.dao.util.SqlDao;
@SqlDao
@PsqlDao
@Repository
-public class PsqlEventInsertRepository extends EventInsertRepository {
+public class PsqlEventInsertRepository extends AbstractEventInsertRepository {
private static final String P_KEY_CONFLICT_STATEMENT = "(id)";
private static final String UNQ_KEY_CONFLICT_STATEMENT = "(tenant_id, entity_type, entity_id, event_type, event_uid)";
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractInsertRepository.java
new file mode 100644
index 0000000000..e8134ca1a1
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractInsertRepository.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright © 2016-2019 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.sqlts;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.support.TransactionTemplate;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import java.util.regex.Pattern;
+
+@Repository
+public abstract class AbstractInsertRepository {
+
+ private static final ThreadLocal PATTERN_THREAD_LOCAL = ThreadLocal.withInitial(() -> Pattern.compile(String.valueOf(Character.MIN_VALUE)));
+ private static final String EMPTY_STR = "";
+
+ protected static final String BOOL_V = "bool_v";
+ protected static final String STR_V = "str_v";
+ protected static final String LONG_V = "long_v";
+ protected static final String DBL_V = "dbl_v";
+
+ protected static final String TS_KV_LATEST_TABLE = "ts_kv_latest";
+ protected static final String TS_KV_TABLE = "ts_kv";
+
+ protected static final String HSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, BOOL_V);
+ protected static final String HSQL_ON_STR_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, STR_V);
+ protected static final String HSQL_ON_LONG_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, LONG_V);
+ protected static final String HSQL_ON_DBL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_TABLE, DBL_V);
+
+ protected static final String HSQL_LATEST_ON_BOOL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, BOOL_V);
+ protected static final String HSQL_LATEST_ON_STR_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, STR_V);
+ protected static final String HSQL_LATEST_ON_LONG_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, LONG_V);
+ protected static final String HSQL_LATEST_ON_DBL_VALUE_UPDATE_SET_NULLS = getHsqlNullValues(TS_KV_LATEST_TABLE, DBL_V);
+
+ protected static final String PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, dbl_v = null";
+ protected static final String PSQL_ON_STR_VALUE_UPDATE_SET_NULLS = "bool_v = null, long_v = null, dbl_v = null";
+ protected static final String PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS = "str_v = null, bool_v = null, dbl_v = null";
+ protected static final String PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, bool_v = null";
+
+ @Value("${sql.remove_null_chars}")
+ private boolean removeNullChars;
+
+ @PersistenceContext
+ protected EntityManager entityManager;
+
+ @Autowired
+ protected JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ protected TransactionTemplate transactionTemplate;
+
+ protected static String getInsertOrUpdateStringHsql(String tableName, String constraint, String value, String nullValues) {
+ return "MERGE INTO " + tableName + " USING(VALUES :entity_type, :entity_id, :key, :ts, :" + value + ") A (entity_type, entity_id, key, ts, " + value + ") ON " + constraint + " WHEN MATCHED THEN UPDATE SET " + tableName + "." + value + " = A." + value + ", " + tableName + ".ts = A.ts," + nullValues + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, key, ts, " + value + ") VALUES (A.entity_type, A.entity_id, A.key, A.ts, A." + value + ")";
+ }
+
+ protected static String getInsertOrUpdateStringPsql(String tableName, String constraint, String value, String nullValues) {
+ return "INSERT INTO " + tableName + " (entity_type, entity_id, key, ts, " + value + ") VALUES (:entity_type, :entity_id, :key, :ts, :" + value + ") ON CONFLICT " + constraint + " DO UPDATE SET " + value + " = :" + value + ", ts = :ts," + nullValues;
+ }
+
+ private static String getHsqlNullValues(String tableName, String notNullValue) {
+ switch (notNullValue) {
+ case BOOL_V:
+ return " " + tableName + ".str_v = null, " + tableName + ".long_v = null, " + tableName + ".dbl_v = null ";
+ case STR_V:
+ return " " + tableName + ".bool_v = null, " + tableName + ".long_v = null, " + tableName + ".dbl_v = null ";
+ case LONG_V:
+ return " " + tableName + ".str_v = null, " + tableName + ".bool_v = null, " + tableName + ".dbl_v = null ";
+ case DBL_V:
+ return " " + tableName + ".str_v = null, " + tableName + ".long_v = null, " + tableName + ".bool_v = null ";
+ default:
+ throw new RuntimeException("Unsupported insert value: [" + notNullValue + "]");
+ }
+ }
+
+ protected String replaceNullChars(String strValue) {
+ if (removeNullChars && strValue != null) {
+ return PATTERN_THREAD_LOCAL.get().matcher(strValue).replaceAll(EMPTY_STR);
+ }
+ return strValue;
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractLatestInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractLatestInsertRepository.java
new file mode 100644
index 0000000000..e9b10eafa3
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractLatestInsertRepository.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2019 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.sqlts;
+
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.stereotype.Repository;
+import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestEntity;
+
+import java.util.List;
+
+@Repository
+public abstract class AbstractLatestInsertRepository extends AbstractInsertRepository {
+
+ public abstract void saveOrUpdate(TsKvLatestEntity entity);
+
+ public abstract void saveOrUpdate(List entities);
+
+ protected void processSaveOrUpdate(TsKvLatestEntity entity, String requestBoolValue, String requestStrValue, String requestLongValue, String requestDblValue) {
+ if (entity.getBooleanValue() != null) {
+ saveOrUpdateBoolean(entity, requestBoolValue);
+ }
+ if (entity.getStrValue() != null) {
+ saveOrUpdateString(entity, requestStrValue);
+ }
+ if (entity.getLongValue() != null) {
+ saveOrUpdateLong(entity, requestLongValue);
+ }
+ if (entity.getDoubleValue() != null) {
+ saveOrUpdateDouble(entity, requestDblValue);
+ }
+ }
+
+ @Modifying
+ protected abstract void saveOrUpdateBoolean(TsKvLatestEntity entity, String query);
+
+ @Modifying
+ protected abstract void saveOrUpdateString(TsKvLatestEntity entity, String query);
+
+ @Modifying
+ protected abstract void saveOrUpdateLong(TsKvLatestEntity entity, String query);
+
+ @Modifying
+ protected abstract void saveOrUpdateDouble(TsKvLatestEntity entity, String query);
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java
index cf3596a864..a7efad1e88 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java
@@ -70,7 +70,7 @@ public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningEx
if (poolSize <= 0) {
poolSize = maximumPoolSize * 4;
}
- insertService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(poolSize));
+ insertService = MoreExecutors.listeningDecorator(Executors.newWorkStealingPool(poolSize));
break;
}
}
@@ -127,4 +127,4 @@ public abstract class AbstractSqlTimeseriesDao extends JpaAbstractDaoListeningEx
Aggregation.NONE, DESC_ORDER);
return findAllAsync(tenantId, entityId, findNewLatestQuery);
}
-}
\ No newline at end of file
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java
index 608933c3f7..4787cd7a46 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractTimeseriesInsertRepository.java
@@ -17,24 +17,17 @@ package org.thingsboard.server.dao.sqlts;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.stereotype.Repository;
-import org.thingsboard.server.dao.model.sql.AbsractTsKvEntity;
+import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
-import javax.persistence.EntityManager;
-import javax.persistence.PersistenceContext;
+import java.util.List;
@Repository
-public abstract class AbstractTimeseriesInsertRepository {
-
- protected static final String BOOL_V = "bool_v";
- protected static final String STR_V = "str_v";
- protected static final String LONG_V = "long_v";
- protected static final String DBL_V = "dbl_v";
-
- @PersistenceContext
- protected EntityManager entityManager;
+public abstract class AbstractTimeseriesInsertRepository extends AbstractInsertRepository {
public abstract void saveOrUpdate(T entity);
+ public abstract void saveOrUpdate(List entities);
+
protected void processSaveOrUpdate(T entity, String requestBoolValue, String requestStrValue, String requestLongValue, String requestDblValue) {
if (entity.getBooleanValue() != null) {
saveOrUpdateBoolean(entity, requestBoolValue);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleInsertRepository.java
index 3c87e9f909..b47c79cb98 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleInsertRepository.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.sqlts.timescale;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvEntity;
@@ -22,27 +23,81 @@ import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.List;
+
@TimescaleDBTsDao
@PsqlDao
@Repository
@Transactional
public class TimescaleInsertRepository extends AbstractTimeseriesInsertRepository {
- private static final String ON_BOOL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, dbl_v = null";
- private static final String ON_STR_VALUE_UPDATE_SET_NULLS = "bool_v = null, long_v = null, dbl_v = null";
- private static final String ON_LONG_VALUE_UPDATE_SET_NULLS = "str_v = null, bool_v = null, dbl_v = null";
- private static final String ON_DBL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, bool_v = null";
+ private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, PSQL_ON_STR_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V, PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS);
+
+ private static final String BATCH_UPDATE =
+ "UPDATE tenant_ts_kv SET bool_v = ?, str_v = ?, long_v = ?, dbl_v = ? WHERE entity_type = ? AND entity_id = ? and key = ? and ts = ?";
+
- private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, ON_BOOL_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, ON_STR_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V , ON_LONG_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE =
+ "INSERT INTO tenant_ts_kv (tenant_id, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) VALUES(?, ?, ?, ?, ?, ?, ?, ?) " +
+ "ON CONFLICT (tenant_id, entity_id, key, ts) DO UPDATE SET bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?;";
@Override
public void saveOrUpdate(TimescaleTsKvEntity entity) {
processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT);
}
+ @Override
+ public void saveOrUpdate(List entities) {
+ jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setString(1, entities.get(i).getTenantId());
+ ps.setString(2, entities.get(i).getEntityId());
+ ps.setString(3, entities.get(i).getKey());
+ ps.setLong(4, entities.get(i).getTs());
+
+ if (entities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(5, entities.get(i).getBooleanValue());
+ ps.setBoolean(9, entities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(5, Types.BOOLEAN);
+ ps.setNull(9, Types.BOOLEAN);
+ }
+
+ ps.setString(6, replaceNullChars(entities.get(i).getStrValue()));
+ ps.setString(10, replaceNullChars(entities.get(i).getStrValue()));
+
+
+ if (entities.get(i).getLongValue() != null) {
+ ps.setLong(7, entities.get(i).getLongValue());
+ ps.setLong(11, entities.get(i).getLongValue());
+ } else {
+ ps.setNull(7, Types.BIGINT);
+ ps.setNull(11, Types.BIGINT);
+ }
+
+ if (entities.get(i).getDoubleValue() != null) {
+ ps.setDouble(8, entities.get(i).getDoubleValue());
+ ps.setDouble(12, entities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(8, Types.DOUBLE);
+ ps.setNull(12, Types.DOUBLE);
+ }
+ }
+
+ @Override
+ public int getBatchSize() {
+ return entities.size();
+ }
+ });
+ }
+
@Override
protected void saveOrUpdateBoolean(TimescaleTsKvEntity entity, String query) {
entityManager.createNativeQuery(query)
@@ -61,7 +116,7 @@ public class TimescaleInsertRepository extends AbstractTimeseriesInsertRepositor
.setParameter("entity_id", entity.getEntityId())
.setParameter("key", entity.getKey())
.setParameter("ts", entity.getTs())
- .setParameter("str_v", entity.getStrValue())
+ .setParameter("str_v", replaceNullChars(entity.getStrValue()))
.executeUpdate();
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
index 844f22a31c..961f545567 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
@@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
@@ -36,11 +37,16 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sqlts.timescale.TimescaleTsKvEntity;
+import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
+import org.thingsboard.server.dao.sql.TbSqlBlockingQueue;
+import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -66,6 +72,39 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
@Autowired
private AbstractTimeseriesInsertRepository insertRepository;
+ @Autowired
+ ScheduledLogExecutorComponent logExecutor;
+
+ @Value("${sql.ts_timescale.batch_size:1000}")
+ private int batchSize;
+
+ @Value("${sql.ts_timescale.batch_max_delay:100}")
+ private long maxDelay;
+
+ @Value("${sql.ts_timescale.stats_print_interval_ms:1000}")
+ private long statsPrintIntervalMs;
+
+ private TbSqlBlockingQueue queue;
+
+ @PostConstruct
+ private void init() {
+ TbSqlBlockingQueueParams params = TbSqlBlockingQueueParams.builder()
+ .logName("TS Timescale")
+ .batchSize(batchSize)
+ .maxDelay(maxDelay)
+ .statsPrintIntervalMs(statsPrintIntervalMs)
+ .build();
+ queue = new TbSqlBlockingQueue<>(params);
+ queue.init(logExecutor, v -> insertRepository.saveOrUpdate(v));
+ }
+
+ @PreDestroy
+ private void destroy() {
+ if (queue != null) {
+ queue.destroy();
+ }
+ }
+
@Override
public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) {
return processFindAllAsync(tenantId, entityId, queries);
@@ -126,11 +165,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
- log.trace("Saving entity to timescale db: {}", entity);
- return insertService.submit(() -> {
- insertRepository.saveOrUpdate(entity);
- return null;
- });
+ return queue.add(entity);
}
@Override
@@ -209,7 +244,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) {
List> result = new ArrayList<>();
timescaleTsKvEntities.forEach(entity -> {
- if(entity != null && entity.isNotEmpty()) {
+ if (entity != null && entity.isNotEmpty()) {
entity.setEntityId(entityIdStr);
entity.setTenantId(tenantIdStr);
entity.setKey(key);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlLatestInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlLatestInsertRepository.java
new file mode 100644
index 0000000000..12c9f9c0a4
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlLatestInsertRepository.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright © 2016-2019 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.sqlts.ts;
+
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestEntity;
+import org.thingsboard.server.dao.sqlts.AbstractLatestInsertRepository;
+import org.thingsboard.server.dao.util.HsqlDao;
+import org.thingsboard.server.dao.util.SqlTsDao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.List;
+
+@SqlTsDao
+@HsqlDao
+@Repository
+@Transactional
+public class HsqlLatestInsertRepository extends AbstractLatestInsertRepository {
+
+ private static final String TS_KV_LATEST_CONSTRAINT = "(ts_kv_latest.entity_type=A.entity_type AND ts_kv_latest.entity_id=A.entity_id AND ts_kv_latest.key=A.key)";
+
+ private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, BOOL_V, HSQL_LATEST_ON_BOOL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, STR_V, HSQL_LATEST_ON_STR_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, LONG_V, HSQL_LATEST_ON_LONG_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, DBL_V, HSQL_LATEST_ON_DBL_VALUE_UPDATE_SET_NULLS);
+
+ private static final String INSERT_OR_UPDATE =
+ "MERGE INTO ts_kv_latest USING(VALUES ?, ?, ?, ?, ?, ?, ?, ?) " +
+ "T (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " +
+ "ON (ts_kv_latest.entity_type=T.entity_type " +
+ "AND ts_kv_latest.entity_id=T.entity_id " +
+ "AND ts_kv_latest.key=T.key) " +
+ "WHEN MATCHED THEN UPDATE SET ts_kv_latest.ts = T.ts, ts_kv_latest.bool_v = T.bool_v, ts_kv_latest.str_v = T.str_v, ts_kv_latest.long_v = T.long_v, ts_kv_latest.dbl_v = T.dbl_v " +
+ "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " +
+ "VALUES (T.entity_type, T.entity_id, T.key, T.ts, T.bool_v, T.str_v, T.long_v, T.dbl_v);";
+
+ @Override
+ public void saveOrUpdate(TsKvLatestEntity entity) {
+ processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT);
+ }
+
+ @Override
+ public void saveOrUpdate(List entities) {
+ jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setString(1, entities.get(i).getEntityType().name());
+ ps.setString(2, entities.get(i).getEntityId());
+ ps.setString(3, entities.get(i).getKey());
+ ps.setLong(4, entities.get(i).getTs());
+
+ if (entities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(5, entities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(5, Types.BOOLEAN);
+ }
+
+ ps.setString(6, entities.get(i).getStrValue());
+
+ if (entities.get(i).getLongValue() != null) {
+ ps.setLong(7, entities.get(i).getLongValue());
+ } else {
+ ps.setNull(7, Types.BIGINT);
+ }
+
+ if (entities.get(i).getDoubleValue() != null) {
+ ps.setDouble(8, entities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(8, Types.DOUBLE);
+ }
+ }
+
+ @Override
+ public int getBatchSize() {
+ return entities.size();
+ }
+ });
+ }
+
+ @Override
+ protected void saveOrUpdateBoolean(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("bool_v", entity.getBooleanValue())
+ .executeUpdate();
+ }
+
+ @Override
+ protected void saveOrUpdateString(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("str_v", entity.getStrValue())
+ .executeUpdate();
+ }
+
+ @Override
+ protected void saveOrUpdateLong(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("long_v", entity.getLongValue())
+ .executeUpdate();
+ }
+
+ @Override
+ protected void saveOrUpdateDouble(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("dbl_v", entity.getDoubleValue())
+ .executeUpdate();
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java
index 3ac5cc67a9..5d27b0d06f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/HsqlTimeseriesInsertRepository.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.sqlts.ts;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
@@ -22,31 +23,78 @@ import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository;
import org.thingsboard.server.dao.util.HsqlDao;
import org.thingsboard.server.dao.util.SqlTsDao;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.List;
+
@SqlTsDao
@HsqlDao
@Repository
@Transactional
public class HsqlTimeseriesInsertRepository extends AbstractTimeseriesInsertRepository {
- private static final String ON_BOOL_VALUE_UPDATE_SET_NULLS = " ts_kv.str_v = null, ts_kv.long_v = null, ts_kv.dbl_v = null ";
- private static final String ON_STR_VALUE_UPDATE_SET_NULLS = " ts_kv.bool_v = null, ts_kv.long_v = null, ts_kv.dbl_v = null ";
- private static final String ON_LONG_VALUE_UPDATE_SET_NULLS = " ts_kv.str_v = null, ts_kv.bool_v = null, ts_kv.dbl_v = null ";
- private static final String ON_DBL_VALUE_UPDATE_SET_NULLS = " ts_kv.str_v = null, ts_kv.long_v = null, ts_kv.bool_v = null ";
+ private static final String TS_KV_CONSTRAINT = "(ts_kv.entity_type=A.entity_type AND ts_kv.entity_id=A.entity_id AND ts_kv.key=A.key AND ts_kv.ts=A.ts)";
- private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, ON_BOOL_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, ON_STR_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V , ON_LONG_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, BOOL_V, HSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, STR_V, HSQL_ON_STR_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, LONG_V, HSQL_ON_LONG_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringHsql(TS_KV_TABLE, TS_KV_CONSTRAINT, DBL_V, HSQL_ON_DBL_VALUE_UPDATE_SET_NULLS);
- private static String getInsertOrUpdateString(String value, String nullValues) {
- return "MERGE INTO ts_kv USING(VALUES :entity_type, :entity_id, :key, :ts, :" + value + ") A (entity_type, entity_id, key, ts, " + value + ") ON (ts_kv.entity_type=A.entity_type AND ts_kv.entity_id=A.entity_id AND ts_kv.key=A.key AND ts_kv.ts=A.ts) WHEN MATCHED THEN UPDATE SET ts_kv." + value + " = A." + value + ", ts_kv.ts = A.ts," + nullValues + "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, key, ts, " + value + ") VALUES (A.entity_type, A.entity_id, A.key, A.ts, A." + value + ")";
- }
+ private static final String INSERT_OR_UPDATE =
+ "MERGE INTO ts_kv USING(VALUES ?, ?, ?, ?, ?, ?, ?, ?) " +
+ "T (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " +
+ "ON (ts_kv.entity_type=T.entity_type " +
+ "AND ts_kv.entity_id=T.entity_id " +
+ "AND ts_kv.key=T.key " +
+ "AND ts_kv.ts=T.ts) " +
+ "WHEN MATCHED THEN UPDATE SET ts_kv.bool_v = T.bool_v, ts_kv.str_v = T.str_v, ts_kv.long_v = T.long_v, ts_kv.dbl_v = T.dbl_v " +
+ "WHEN NOT MATCHED THEN INSERT (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) " +
+ "VALUES (T.entity_type, T.entity_id, T.key, T.ts, T.bool_v, T.str_v, T.long_v, T.dbl_v);";
@Override
public void saveOrUpdate(TsKvEntity entity) {
processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT);
}
+ @Override
+ public void saveOrUpdate(List entities) {
+ jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setString(1, entities.get(i).getEntityType().name());
+ ps.setString(2, entities.get(i).getEntityId());
+ ps.setString(3, entities.get(i).getKey());
+ ps.setLong(4, entities.get(i).getTs());
+
+ if (entities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(5, entities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(5, Types.BOOLEAN);
+ }
+
+ ps.setString(6, entities.get(i).getStrValue());
+
+ if (entities.get(i).getLongValue() != null) {
+ ps.setLong(7, entities.get(i).getLongValue());
+ } else {
+ ps.setNull(7, Types.BIGINT);
+ }
+
+ if (entities.get(i).getDoubleValue() != null) {
+ ps.setDouble(8, entities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(8, Types.DOUBLE);
+ }
+ }
+
+ @Override
+ public int getBatchSize() {
+ return entities.size();
+ }
+ });
+ }
+
@Override
protected void saveOrUpdateBoolean(TsKvEntity entity, String query) {
entityManager.createNativeQuery(query)
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java
index 9e4e281ebd..7c198d9a73 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/JpaTimeseriesDao.java
@@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
@@ -38,6 +39,10 @@ import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestCompositeKey;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestEntity;
+import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
+import org.thingsboard.server.dao.sql.TbSqlBlockingQueue;
+import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
+import org.thingsboard.server.dao.sqlts.AbstractLatestInsertRepository;
import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository;
import org.thingsboard.server.dao.timeseries.SimpleListenableFuture;
@@ -45,6 +50,8 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.dao.util.SqlTsDao;
import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -69,6 +76,66 @@ public class JpaTimeseriesDao extends AbstractSqlTimeseriesDao implements Timese
@Autowired
private AbstractTimeseriesInsertRepository insertRepository;
+ @Autowired
+ private AbstractLatestInsertRepository insertLatestRepository;
+
+ @Autowired
+ ScheduledLogExecutorComponent logExecutor;
+
+ @Value("${sql.ts.batch_size:1000}")
+ private int tsBatchSize;
+
+ @Value("${sql.ts.batch_max_delay:100}")
+ private long tsMaxDelay;
+
+ @Value("${sql.ts.stats_print_interval_ms:1000}")
+ private long tsStatsPrintIntervalMs;
+
+ @Value("${sql.ts_latest.batch_size:1000}")
+ private int tsLatestBatchSize;
+
+ @Value("${sql.ts_latest.batch_max_delay:100}")
+ private long tsLatestMaxDelay;
+
+ @Value("${sql.ts_latest.stats_print_interval_ms:1000}")
+ private long tsLatestStatsPrintIntervalMs;
+
+ private TbSqlBlockingQueue tsQueue;
+ private TbSqlBlockingQueue tsLatestQueue;
+
+
+ @PostConstruct
+ private void init() {
+ TbSqlBlockingQueueParams tsParams = TbSqlBlockingQueueParams.builder()
+ .logName("TS")
+ .batchSize(tsBatchSize)
+ .maxDelay(tsMaxDelay)
+ .statsPrintIntervalMs(tsStatsPrintIntervalMs)
+ .build();
+ tsQueue = new TbSqlBlockingQueue<>(tsParams);
+ tsQueue.init(logExecutor, v -> insertRepository.saveOrUpdate(v));
+
+ TbSqlBlockingQueueParams tsLatestParams = TbSqlBlockingQueueParams.builder()
+ .logName("TS Latest")
+ .batchSize(tsLatestBatchSize)
+ .maxDelay(tsLatestMaxDelay)
+ .statsPrintIntervalMs(tsLatestStatsPrintIntervalMs)
+ .build();
+ tsLatestQueue = new TbSqlBlockingQueue<>(tsLatestParams);
+ tsLatestQueue.init(logExecutor, v -> insertLatestRepository.saveOrUpdate(v));
+ }
+
+ @PreDestroy
+ private void destroy() {
+ if (tsQueue != null) {
+ tsQueue.destroy();
+ }
+
+ if (tsLatestQueue != null) {
+ tsLatestQueue.destroy();
+ }
+ }
+
@Override
public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) {
return processFindAllAsync(tenantId, entityId, queries);
@@ -262,10 +329,7 @@ public class JpaTimeseriesDao extends AbstractSqlTimeseriesDao implements Timese
entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
log.trace("Saving entity: {}", entity);
- return insertService.submit(() -> {
- insertRepository.saveOrUpdate(entity);
- return null;
- });
+ return tsQueue.add(entity);
}
@Override
@@ -284,10 +348,7 @@ public class JpaTimeseriesDao extends AbstractSqlTimeseriesDao implements Timese
latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null));
latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
- return insertService.submit(() -> {
- tsKvLatestRepository.save(latestEntity);
- return null;
- });
+ return tsLatestQueue.add(latestEntity);
}
@Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlLatestInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlLatestInsertRepository.java
new file mode 100644
index 0000000000..92252e9d18
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlLatestInsertRepository.java
@@ -0,0 +1,205 @@
+/**
+ * Copyright © 2016-2019 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.sqlts.ts;
+
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionCallbackWithoutResult;
+import org.thingsboard.server.dao.model.sqlts.ts.TsKvLatestEntity;
+import org.thingsboard.server.dao.sqlts.AbstractLatestInsertRepository;
+import org.thingsboard.server.dao.util.PsqlDao;
+import org.thingsboard.server.dao.util.SqlTsDao;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+
+@SqlTsDao
+@PsqlDao
+@Repository
+@Transactional
+public class PsqlLatestInsertRepository extends AbstractLatestInsertRepository {
+
+ private static final String TS_KV_LATEST_CONSTRAINT = "(entity_type, entity_id, key)";
+
+ private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, BOOL_V, PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, STR_V, PSQL_ON_STR_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, LONG_V, PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_LATEST_TABLE, TS_KV_LATEST_CONSTRAINT, DBL_V, PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS);
+
+ private static final String BATCH_UPDATE =
+ "UPDATE ts_kv_latest SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ? WHERE entity_type = ? AND entity_id = ? and key = ?";
+
+
+ private static final String INSERT_OR_UPDATE =
+ "INSERT INTO ts_kv_latest (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) VALUES(?, ?, ?, ?, ?, ?, ?, ?) " +
+ "ON CONFLICT (entity_type, entity_id, key) DO UPDATE SET ts = ?, bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?;";
+
+ @Override
+ public void saveOrUpdate(TsKvLatestEntity entity) {
+ processSaveOrUpdate(entity, INSERT_OR_UPDATE_BOOL_STATEMENT, INSERT_OR_UPDATE_STR_STATEMENT, INSERT_OR_UPDATE_LONG_STATEMENT, INSERT_OR_UPDATE_DBL_STATEMENT);
+ }
+
+ @Override
+ public void saveOrUpdate(List entities) {
+ transactionTemplate.execute(new TransactionCallbackWithoutResult() {
+ @Override
+ protected void doInTransactionWithoutResult(TransactionStatus status) {
+ int[] result = jdbcTemplate.batchUpdate(BATCH_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setLong(1, entities.get(i).getTs());
+
+ if (entities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(2, entities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(2, Types.BOOLEAN);
+ }
+
+ ps.setString(3, replaceNullChars(entities.get(i).getStrValue()));
+
+ if (entities.get(i).getLongValue() != null) {
+ ps.setLong(4, entities.get(i).getLongValue());
+ } else {
+ ps.setNull(4, Types.BIGINT);
+ }
+
+ if (entities.get(i).getDoubleValue() != null) {
+ ps.setDouble(5, entities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(5, Types.DOUBLE);
+ }
+
+ ps.setString(6, entities.get(i).getEntityType().name());
+ ps.setString(7, entities.get(i).getEntityId());
+ ps.setString(8, entities.get(i).getKey());
+ }
+
+ @Override
+ public int getBatchSize() {
+ return entities.size();
+ }
+ });
+
+ int updatedCount = 0;
+ for (int i = 0; i < result.length; i++) {
+ if (result[i] == 0) {
+ updatedCount++;
+ }
+ }
+
+ List insertEntities = new ArrayList<>(updatedCount);
+ for (int i = 0; i < result.length; i++) {
+ if (result[i] == 0) {
+ insertEntities.add(entities.get(i));
+ }
+ }
+
+ jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setString(1, insertEntities.get(i).getEntityType().name());
+ ps.setString(2, insertEntities.get(i).getEntityId());
+ ps.setString(3, insertEntities.get(i).getKey());
+ ps.setLong(4, insertEntities.get(i).getTs());
+ ps.setLong(9, insertEntities.get(i).getTs());
+
+ if (insertEntities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(5, insertEntities.get(i).getBooleanValue());
+ ps.setBoolean(10, insertEntities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(5, Types.BOOLEAN);
+ ps.setNull(10, Types.BOOLEAN);
+ }
+
+ ps.setString(6, replaceNullChars(entities.get(i).getStrValue()));
+ ps.setString(11, replaceNullChars(entities.get(i).getStrValue()));
+
+
+ if (insertEntities.get(i).getLongValue() != null) {
+ ps.setLong(7, insertEntities.get(i).getLongValue());
+ ps.setLong(12, insertEntities.get(i).getLongValue());
+ } else {
+ ps.setNull(7, Types.BIGINT);
+ ps.setNull(12, Types.BIGINT);
+ }
+
+ if (insertEntities.get(i).getDoubleValue() != null) {
+ ps.setDouble(8, insertEntities.get(i).getDoubleValue());
+ ps.setDouble(13, insertEntities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(8, Types.DOUBLE);
+ ps.setNull(13, Types.DOUBLE);
+ }
+ }
+
+ @Override
+ public int getBatchSize() {
+ return insertEntities.size();
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ protected void saveOrUpdateBoolean(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("bool_v", entity.getBooleanValue())
+ .executeUpdate();
+ }
+
+ @Override
+ protected void saveOrUpdateString(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("str_v", replaceNullChars(entity.getStrValue()))
+ .executeUpdate();
+ }
+
+ @Override
+ protected void saveOrUpdateLong(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("long_v", entity.getLongValue())
+ .executeUpdate();
+ }
+
+ @Override
+ protected void saveOrUpdateDouble(TsKvLatestEntity entity, String query) {
+ entityManager.createNativeQuery(query)
+ .setParameter("entity_type", entity.getEntityType().name())
+ .setParameter("entity_id", entity.getEntityId())
+ .setParameter("key", entity.getKey())
+ .setParameter("ts", entity.getTs())
+ .setParameter("dbl_v", entity.getDoubleValue())
+ .executeUpdate();
+ }
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlTimeseriesInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlTimeseriesInsertRepository.java
index 4ed91c28f2..edc37822b1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlTimeseriesInsertRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/PsqlTimeseriesInsertRepository.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.sqlts.ts;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
@@ -22,25 +23,27 @@ import org.thingsboard.server.dao.sqlts.AbstractTimeseriesInsertRepository;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlTsDao;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.List;
+
@SqlTsDao
@PsqlDao
@Repository
@Transactional
public class PsqlTimeseriesInsertRepository extends AbstractTimeseriesInsertRepository {
- private static final String ON_BOOL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, dbl_v = null";
- private static final String ON_STR_VALUE_UPDATE_SET_NULLS = "bool_v = null, long_v = null, dbl_v = null";
- private static final String ON_LONG_VALUE_UPDATE_SET_NULLS = "str_v = null, bool_v = null, dbl_v = null";
- private static final String ON_DBL_VALUE_UPDATE_SET_NULLS = "str_v = null, long_v = null, bool_v = null";
+ private static final String TS_KV_CONSTRAINT = "(entity_type, entity_id, key, ts)";
- private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateString(BOOL_V, ON_BOOL_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateString(STR_V, ON_STR_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateString(LONG_V , ON_LONG_VALUE_UPDATE_SET_NULLS);
- private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateString(DBL_V, ON_DBL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_BOOL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, BOOL_V, PSQL_ON_BOOL_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_STR_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, STR_V, PSQL_ON_STR_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_LONG_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, LONG_V, PSQL_ON_LONG_VALUE_UPDATE_SET_NULLS);
+ private static final String INSERT_OR_UPDATE_DBL_STATEMENT = getInsertOrUpdateStringPsql(TS_KV_TABLE, TS_KV_CONSTRAINT, DBL_V, PSQL_ON_DBL_VALUE_UPDATE_SET_NULLS);
- private static String getInsertOrUpdateString(String value, String nullValues) {
- return "INSERT INTO ts_kv (entity_type, entity_id, key, ts, " + value + ") VALUES (:entity_type, :entity_id, :key, :ts, :" + value + ") ON CONFLICT (entity_type, entity_id, key, ts) DO UPDATE SET " + value + " = :" + value + ", ts = :ts," + nullValues;
- }
+ private static final String INSERT_OR_UPDATE =
+ "INSERT INTO ts_kv (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v) VALUES(?, ?, ?, ?, ?, ?, ?, ?) " +
+ "ON CONFLICT (entity_type, entity_id, key, ts) DO UPDATE SET bool_v = ?, str_v = ?, long_v = ?, dbl_v = ?;";
@Override
public void saveOrUpdate(TsKvEntity entity) {
@@ -65,7 +68,7 @@ public class PsqlTimeseriesInsertRepository extends AbstractTimeseriesInsertRepo
.setParameter("entity_id", entity.getEntityId())
.setParameter("key", entity.getKey())
.setParameter("ts", entity.getTs())
- .setParameter("str_v", entity.getStrValue())
+ .setParameter("str_v", replaceNullChars(entity.getStrValue()))
.executeUpdate();
}
@@ -90,4 +93,50 @@ public class PsqlTimeseriesInsertRepository extends AbstractTimeseriesInsertRepo
.setParameter("dbl_v", entity.getDoubleValue())
.executeUpdate();
}
+
+ @Override
+ public void saveOrUpdate(List entities) {
+ jdbcTemplate.batchUpdate(INSERT_OR_UPDATE, new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setString(1, entities.get(i).getEntityType().name());
+ ps.setString(2, entities.get(i).getEntityId());
+ ps.setString(3, entities.get(i).getKey());
+ ps.setLong(4, entities.get(i).getTs());
+
+ if (entities.get(i).getBooleanValue() != null) {
+ ps.setBoolean(5, entities.get(i).getBooleanValue());
+ ps.setBoolean(9, entities.get(i).getBooleanValue());
+ } else {
+ ps.setNull(5, Types.BOOLEAN);
+ ps.setNull(9, Types.BOOLEAN);
+ }
+
+ ps.setString(6, replaceNullChars(entities.get(i).getStrValue()));
+ ps.setString(10, replaceNullChars(entities.get(i).getStrValue()));
+
+
+ if (entities.get(i).getLongValue() != null) {
+ ps.setLong(7, entities.get(i).getLongValue());
+ ps.setLong(11, entities.get(i).getLongValue());
+ } else {
+ ps.setNull(7, Types.BIGINT);
+ ps.setNull(11, Types.BIGINT);
+ }
+
+ if (entities.get(i).getDoubleValue() != null) {
+ ps.setDouble(8, entities.get(i).getDoubleValue());
+ ps.setDouble(12, entities.get(i).getDoubleValue());
+ } else {
+ ps.setNull(8, Types.DOUBLE);
+ ps.setNull(12, Types.DOUBLE);
+ }
+ }
+
+ @Override
+ public int getBatchSize() {
+ return entities.size();
+ }
+ });
+ }
}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
index c22321ee52..a553aa5f9c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
@@ -44,6 +44,7 @@ public abstract class AbstractBufferedRateExecutor perTenantLimits = new ConcurrentHashMap<>();
@@ -57,12 +58,14 @@ public abstract class AbstractBufferedRateExecutor(queueLimit);
this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads);
this.callbackExecutor = Executors.newWorkStealingPool(callbackThreads);
@@ -131,6 +134,13 @@ public abstract class AbstractBufferedRateExecutor finalTaskCtx = taskCtx;
+ if (printQueriesFreq > 0) {
+ if (printQueriesIdx.incrementAndGet() >= printQueriesFreq) {
+ printQueriesIdx.set(0);
+ String query = queryToString(finalTaskCtx);
+ log.info("[{}] Cassandra query: {}", taskCtx.getId(), query);
+ }
+ }
logTask("Processing", finalTaskCtx);
concurrencyLevel.incrementAndGet();
long timeout = finalTaskCtx.getCreateTime() + maxWaitTime - System.currentTimeMillis();
@@ -187,17 +197,8 @@ public abstract class AbstractBufferedRateExecutor taskCtx) {
if (log.isTraceEnabled()) {
if (taskCtx.getTask() instanceof CassandraStatementTask) {
- CassandraStatementTask cassStmtTask = (CassandraStatementTask) taskCtx.getTask();
- if (cassStmtTask.getStatement() instanceof BoundStatement) {
- BoundStatement stmt = (BoundStatement) cassStmtTask.getStatement();
- String query = stmt.preparedStatement().getQueryString();
- try {
- query = toStringWithValues(stmt, ProtocolVersion.V5);
- } catch (Exception e) {
- log.warn("Can't convert to query with values", e);
- }
- log.trace("[{}] {} task: {}, BoundStatement query: {}", taskCtx.getId(), action, taskCtx, query);
- }
+ String query = queryToString(taskCtx);
+ log.trace("[{}] {} task: {}, BoundStatement query: {}", taskCtx.getId(), action, taskCtx, query);
} else {
log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx);
}
@@ -206,6 +207,22 @@ public abstract class AbstractBufferedRateExecutor taskCtx) {
+ CassandraStatementTask cassStmtTask = (CassandraStatementTask) taskCtx.getTask();
+ if (cassStmtTask.getStatement() instanceof BoundStatement) {
+ BoundStatement stmt = (BoundStatement) cassStmtTask.getStatement();
+ String query = stmt.preparedStatement().getQueryString();
+ try {
+ query = toStringWithValues(stmt, ProtocolVersion.V5);
+ } catch (Exception e) {
+ log.warn("Can't convert to query with values", e);
+ }
+ return query;
+ } else {
+ return "Not Cassandra Statement Task";
+ }
+ }
+
private static String toStringWithValues(BoundStatement boundStatement, ProtocolVersion protocolVersion) {
CodecRegistry codecRegistry = boundStatement.preparedStatement().getCodecRegistry();
PreparedStatement preparedStatement = boundStatement.preparedStatement();
diff --git a/dao/src/main/resources/cassandra/schema-entities.cql b/dao/src/main/resources/cassandra/schema-entities.cql
index 611c08d5ef..a07e27cbe1 100644
--- a/dao/src/main/resources/cassandra/schema-entities.cql
+++ b/dao/src/main/resources/cassandra/schema-entities.cql
@@ -244,6 +244,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.asset (
customer_id timeuuid,
name text,
type text,
+ label text,
search_text text,
additional_info text,
PRIMARY KEY (id, tenant_id, customer_id, type)
@@ -711,4 +712,4 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.entity_view_by_tenant_and_ent
AND search_text IS NOT NULL
AND id IS NOT NULL
PRIMARY KEY (tenant_id, entity_id, customer_id, search_text, id, type)
- WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC);
\ No newline at end of file
+ WITH CLUSTERING ORDER BY (entity_id DESC, customer_id DESC, search_text ASC, id DESC);
diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql
index 59785ae758..a0a25ee070 100644
--- a/dao/src/main/resources/sql/schema-entities-idx.sql
+++ b/dao/src/main/resources/sql/schema-entities-idx.sql
@@ -14,10 +14,22 @@
-- limitations under the License.
--
-CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(tenant_id, type, originator_type, originator_id);
+CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type ON alarm(originator_id, type, start_ts DESC);
CREATE INDEX IF NOT EXISTS idx_event_type_entity_id ON event(tenant_id, event_type, entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id);
CREATE INDEX IF NOT EXISTS idx_relation_from_id ON relation(relation_type_group, from_type, from_id);
+
+CREATE INDEX IF NOT EXISTS idx_device_customer_id ON device(tenant_id, customer_id);
+
+CREATE INDEX IF NOT EXISTS idx_device_customer_id_and_type ON device(tenant_id, customer_id, type);
+
+CREATE INDEX IF NOT EXISTS idx_device_type ON device(tenant_id, type);
+
+CREATE INDEX IF NOT EXISTS idx_asset_customer_id ON asset(tenant_id, customer_id);
+
+CREATE INDEX IF NOT EXISTS idx_asset_customer_id_and_type ON asset(tenant_id, customer_id, type);
+
+CREATE INDEX IF NOT EXISTS idx_asset_type ON asset(tenant_id, type);
\ No newline at end of file
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 2903ca2fb1..4b21d851a5 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -42,9 +42,11 @@ CREATE TABLE IF NOT EXISTS asset (
additional_info varchar,
customer_id varchar(31),
name varchar(255),
+ label varchar(255),
search_text varchar(255),
tenant_id varchar(31),
- type varchar(255)
+ type varchar(255),
+ CONSTRAINT asset_name_unq_key UNIQUE (tenant_id, name)
);
CREATE TABLE IF NOT EXISTS audit_log (
@@ -119,7 +121,8 @@ CREATE TABLE IF NOT EXISTS device (
name varchar(255),
label varchar(255),
search_text varchar(255),
- tenant_id varchar(31)
+ tenant_id varchar(31),
+ CONSTRAINT device_name_unq_key UNIQUE (tenant_id, name)
);
CREATE TABLE IF NOT EXISTS device_credentials (
@@ -127,7 +130,8 @@ CREATE TABLE IF NOT EXISTS device_credentials (
credentials_id varchar,
credentials_type varchar(255),
credentials_value varchar,
- device_id varchar(31)
+ device_id varchar(31),
+ CONSTRAINT device_credentials_id_unq_key UNIQUE (credentials_id)
);
CREATE TABLE IF NOT EXISTS event (
diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties
index 263da99a4b..caec9e0e66 100644
--- a/dao/src/test/resources/application-test.properties
+++ b/dao/src/test/resources/application-test.properties
@@ -40,3 +40,5 @@ security.claim.allowClaimingByDefault=true
security.claim.duration=60000
database.ts_max_intervals=700
+
+sql.remove_null_chars=true
diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml
index a6b5d65ff7..c706d0dc37 100644
--- a/msa/black-box-tests/pom.xml
+++ b/msa/black-box-tests/pom.xml
@@ -21,7 +21,7 @@
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
msa
org.thingsboard.msa
diff --git a/msa/js-executor/api/jsExecutor.js b/msa/js-executor/api/jsExecutor.js
index 3568a6c410..7a4ffb04f1 100644
--- a/msa/js-executor/api/jsExecutor.js
+++ b/msa/js-executor/api/jsExecutor.js
@@ -17,11 +17,28 @@
const vm = require('vm');
-function JsExecutor() {
+function JsExecutor(useSandbox) {
+ this.useSandbox = useSandbox;
}
JsExecutor.prototype.compileScript = function(code) {
- return new Promise(function(resolve, reject) {
+ if (this.useSandbox) {
+ return createScript(code);
+ } else {
+ return createFunction(code);
+ }
+}
+
+JsExecutor.prototype.executeScript = function(script, args, timeout) {
+ if (this.useSandbox) {
+ return invokeScript(script, args, timeout);
+ } else {
+ return invokeFunction(script, args);
+ }
+}
+
+function createScript(code) {
+ return new Promise((resolve, reject) => {
try {
code = "("+code+")(...args)";
var script = new vm.Script(code);
@@ -32,8 +49,8 @@ JsExecutor.prototype.compileScript = function(code) {
});
}
-JsExecutor.prototype.executeScript = function(script, args, timeout) {
- return new Promise(function(resolve, reject) {
+function invokeScript(script, args, timeout) {
+ return new Promise((resolve, reject) => {
try {
var sandbox = Object.create(null);
sandbox.args = args;
@@ -45,4 +62,29 @@ JsExecutor.prototype.executeScript = function(script, args, timeout) {
});
}
+
+function createFunction(code) {
+ return new Promise((resolve, reject) => {
+ try {
+ code = "return ("+code+")(...args)";
+ const parsingContext = vm.createContext({});
+ const func = vm.compileFunction(code, ['args'], {parsingContext: parsingContext});
+ resolve(func);
+ } catch (err) {
+ reject(err);
+ }
+ });
+}
+
+function invokeFunction(func, args) {
+ return new Promise((resolve, reject) => {
+ try {
+ var result = func(args);
+ resolve(result);
+ } catch (err) {
+ reject(err);
+ }
+ });
+}
+
module.exports = JsExecutor;
diff --git a/msa/js-executor/api/jsInvokeMessageProcessor.js b/msa/js-executor/api/jsInvokeMessageProcessor.js
index 1a1df75e09..6afc02e89c 100644
--- a/msa/js-executor/api/jsInvokeMessageProcessor.js
+++ b/msa/js-executor/api/jsInvokeMessageProcessor.js
@@ -15,18 +15,22 @@
*/
'use strict';
+const COMPILATION_ERROR = 0;
+const RUNTIME_ERROR = 1;
+const TIMEOUT_ERROR = 2;
+const UNRECOGNIZED = -1;
+
const config = require('config'),
- logger = require('../config/logger')('JsInvokeMessageProcessor'),
+ logger = require('../config/logger')._logger('JsInvokeMessageProcessor'),
Utils = require('./utils'),
- js = require('./jsinvoke.proto').js,
- KeyedMessage = require('kafka-node').KeyedMessage,
JsExecutor = require('./jsExecutor');
const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency'));
+const useSandbox = config.get('script.use_sandbox') === 'true';
function JsInvokeMessageProcessor(producer) {
this.producer = producer;
- this.executor = new JsExecutor();
+ this.executor = new JsExecutor(useSandbox);
this.scriptMap = {};
this.executedScriptsCounter = 0;
}
@@ -34,18 +38,22 @@ function JsInvokeMessageProcessor(producer) {
JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) {
var requestId;
+ var responseTopic;
try {
- var request = js.RemoteJsRequest.decode(message.value);
- requestId = getRequestId(request);
+ var request = JSON.parse(message.value.toString('utf8'));
+ var buf = message.headers['requestId'];
+ requestId = Utils.UUIDFromBuffer(buf);
+ buf = message.headers['responseTopic'];
+ responseTopic = buf.toString('utf8');
- logger.debug('[%s] Received request, responseTopic: [%s]', requestId, request.responseTopic);
+ logger.debug('[%s] Received request, responseTopic: [%s]', requestId, responseTopic);
if (request.compileRequest) {
- this.processCompileRequest(requestId, request.responseTopic, request.compileRequest);
+ this.processCompileRequest(requestId, responseTopic, request.compileRequest);
} else if (request.invokeRequest) {
- this.processInvokeRequest(requestId, request.responseTopic, request.invokeRequest);
+ this.processInvokeRequest(requestId, responseTopic, request.invokeRequest);
} else if (request.releaseRequest) {
- this.processReleaseRequest(requestId, request.responseTopic, request.releaseRequest);
+ this.processReleaseRequest(requestId, responseTopic, request.releaseRequest);
} else {
logger.error('[%s] Unknown request recevied!', requestId);
}
@@ -68,7 +76,7 @@ JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, r
this.sendResponse(requestId, responseTopic, scriptId, compileResponse);
},
(err) => {
- var compileResponse = createCompileResponse(scriptId, false, js.JsInvokeErrorCode.COMPILATION_ERROR, err);
+ var compileResponse = createCompileResponse(scriptId, false, COMPILATION_ERROR, err);
logger.debug('[%s] Sending failed compile response, scriptId: [%s]', requestId, scriptId);
this.sendResponse(requestId, responseTopic, scriptId, compileResponse);
}
@@ -96,9 +104,9 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re
(err) => {
var errorCode;
if (err.message.includes('Script execution timed out')) {
- errorCode = js.JsInvokeErrorCode.TIMEOUT_ERROR;
+ errorCode = TIMEOUT_ERROR;
} else {
- errorCode = js.JsInvokeErrorCode.RUNTIME_ERROR;
+ errorCode = RUNTIME_ERROR;
}
var invokeResponse = createInvokeResponse("", false, errorCode, err);
logger.debug('[%s] Sending failed invoke response, scriptId: [%s], errorCode: [%s]', requestId, scriptId, errorCode);
@@ -107,8 +115,8 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re
)
},
(err) => {
- var invokeResponse = createInvokeResponse("", false, js.JsInvokeErrorCode.COMPILATION_ERROR, err);
- logger.debug('[%s] Sending failed invoke response, scriptId: [%s], errorCode: [%s]', requestId, scriptId, js.JsInvokeErrorCode.COMPILATION_ERROR);
+ var invokeResponse = createInvokeResponse("", false, COMPILATION_ERROR, err);
+ logger.debug('[%s] Sending failed invoke response, scriptId: [%s], errorCode: [%s]', requestId, scriptId, COMPILATION_ERROR);
this.sendResponse(requestId, responseTopic, scriptId, null, invokeResponse);
}
);
@@ -127,15 +135,26 @@ JsInvokeMessageProcessor.prototype.processReleaseRequest = function(requestId, r
JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseTopic, scriptId, compileResponse, invokeResponse, releaseResponse) {
var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse);
- var rawResponse = js.RemoteJsResponse.encode(remoteResponse).finish();
- const message = new KeyedMessage(scriptId, rawResponse);
- const payloads = [ { topic: responseTopic, messages: message, key: scriptId } ];
- this.producer.send(payloads, function (err, data) {
- if (err) {
- logger.error('[%s] Failed to send response to kafka: %s', requestId, err.message);
- logger.error(err.stack);
+ var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8');
+ this.producer.send(
+ {
+ topic: responseTopic,
+ messages: [
+ {
+ key: scriptId,
+ value: rawResponse
+ }
+ ]
}
- });
+ ).then(
+ () => {},
+ (err) => {
+ if (err) {
+ logger.error('[%s] Failed to send response to kafka: %s', requestId, err.message);
+ logger.error(err.stack);
+ }
+ }
+ );
}
JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scriptBody) {
@@ -159,50 +178,42 @@ JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scrip
function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) {
const requestIdBits = Utils.UUIDToBits(requestId);
- return js.RemoteJsResponse.create(
- {
+ return {
requestIdMSB: requestIdBits[0],
requestIdLSB: requestIdBits[1],
compileResponse: compileResponse,
invokeResponse: invokeResponse,
releaseResponse: releaseResponse
- }
- );
+ };
}
function createCompileResponse(scriptId, success, errorCode, err) {
const scriptIdBits = Utils.UUIDToBits(scriptId);
- return js.JsCompileResponse.create(
- {
+ return {
errorCode: errorCode,
success: success,
errorDetails: parseJsErrorDetails(err),
scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1]
- }
- );
+ };
}
function createInvokeResponse(result, success, errorCode, err) {
- return js.JsInvokeResponse.create(
- {
+ return {
errorCode: errorCode,
success: success,
errorDetails: parseJsErrorDetails(err),
result: result
- }
- );
+ };
}
function createReleaseResponse(scriptId, success) {
const scriptIdBits = Utils.UUIDToBits(scriptId);
- return js.JsReleaseResponse.create(
- {
+ return {
success: success,
scriptIdMSB: scriptIdBits[0],
scriptIdLSB: scriptIdBits[1]
- }
- );
+ };
}
function parseJsErrorDetails(err) {
@@ -229,8 +240,4 @@ function getScriptId(request) {
return Utils.toUUIDString(request.scriptIdMSB, request.scriptIdLSB);
}
-function getRequestId(request) {
- return Utils.toUUIDString(request.requestIdMSB, request.requestIdLSB);
-}
-
-module.exports = JsInvokeMessageProcessor;
\ No newline at end of file
+module.exports = JsInvokeMessageProcessor;
diff --git a/msa/js-executor/api/utils.js b/msa/js-executor/api/utils.js
index 40ab74e721..2e0381086b 100644
--- a/msa/js-executor/api/utils.js
+++ b/msa/js-executor/api/utils.js
@@ -18,16 +18,17 @@
const Long = require('long'),
uuidParse = require('uuid-parse');
-var logger = require('../config/logger')('Utils');
-
exports.toUUIDString = function(mostSigBits, leastSigBits) {
var msbBytes = Long.fromValue(mostSigBits, false).toBytes(false);
var lsbBytes = Long.fromValue(leastSigBits, false).toBytes(false);
var uuidBytes = msbBytes.concat(lsbBytes);
- var buff = new Buffer(uuidBytes, 'utf8');
return uuidParse.unparse(uuidBytes);
}
+exports.UUIDFromBuffer = function(buf) {
+ return uuidParse.unparse(buf);
+}
+
exports.UUIDToBits = function(uuidString) {
const bytes = uuidParse.parse(uuidString);
var msb = Long.fromBytes(bytes.slice(0,8), false, false).toString();
diff --git a/msa/js-executor/config/custom-environment-variables.yml b/msa/js-executor/config/custom-environment-variables.yml
index 99f47cb5f1..c3caf15a9a 100644
--- a/msa/js-executor/config/custom-environment-variables.yml
+++ b/msa/js-executor/config/custom-environment-variables.yml
@@ -25,4 +25,5 @@ logger:
filename: "LOGGER_FILENAME"
script:
+ use_sandbox: "SCRIPT_USE_SANDBOX"
script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY"
diff --git a/msa/js-executor/config/default.yml b/msa/js-executor/config/default.yml
index fb470567e7..9688722109 100644
--- a/msa/js-executor/config/default.yml
+++ b/msa/js-executor/config/default.yml
@@ -26,4 +26,5 @@ logger:
filename: "tb-js-executor-%DATE%.log"
script:
+ use_sandbox: "true"
script_body_trace_frequency: "1000"
diff --git a/msa/js-executor/config/logger.js b/msa/js-executor/config/logger.js
index 4f5340c2a8..317dc0cdd5 100644
--- a/msa/js-executor/config/logger.js
+++ b/msa/js-executor/config/logger.js
@@ -17,9 +17,24 @@ var config = require('config'),
path = require('path'),
DailyRotateFile = require('winston-daily-rotate-file');
+const { logLevel } = require('kafkajs');
const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf, splat } = format;
+const toWinstonLogLevel = level => {
+ switch(level) {
+ case logLevel.ERROR:
+ case logLevel.NOTHING:
+ return 'error'
+ case logLevel.WARN:
+ return 'warn'
+ case logLevel.INFO:
+ return 'info'
+ case logLevel.DEBUG:
+ return 'debug'
+ }
+}
+
var loggerTransports = [];
if (process.env.NODE_ENV !== 'production' || process.env.DOCKER_MODE === 'true') {
@@ -56,4 +71,33 @@ function _logger(moduleLabel) {
});
}
-module.exports = _logger;
\ No newline at end of file
+const KafkaJsWinstonLogCreator = logLevel => {
+ const logger = createLogger({
+ level: toWinstonLogLevel(logLevel),
+ format:combine(
+ splat(),
+ label({ label: 'kafkajs' }),
+ timestamp({format: 'YYYY-MM-DD HH:mm:ss,SSS'}),
+ printf(info => {
+ var res = `${info.timestamp} [${info.label}] ${info.level.toUpperCase()}: ${info.message}`;
+ if (info.extra) {
+ res +=`: ${JSON.stringify(info.extra)}`;
+ }
+ return res;
+ }
+ )
+ ),
+ transports: loggerTransports
+ });
+
+ return ({ namespace, level, label, log }) => {
+ const { message, ...extra } = log;
+ logger.log({
+ level: toWinstonLogLevel(level),
+ message,
+ extra,
+ });
+ }
+}
+
+module.exports = {_logger, KafkaJsWinstonLogCreator};
diff --git a/msa/js-executor/package-lock.json b/msa/js-executor/package-lock.json
index c61ef011a2..96c8341882 100644
--- a/msa/js-executor/package-lock.json
+++ b/msa/js-executor/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "thingsboard-js-executor",
- "version": "2.4.1",
+ "version": "2.4.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -35,60 +35,6 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true
},
- "@protobufjs/aspromise": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
- "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78="
- },
- "@protobufjs/base64": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
- "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
- },
- "@protobufjs/codegen": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
- "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
- },
- "@protobufjs/eventemitter": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
- "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A="
- },
- "@protobufjs/fetch": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
- "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=",
- "requires": {
- "@protobufjs/aspromise": "^1.1.1",
- "@protobufjs/inquire": "^1.1.0"
- }
- },
- "@protobufjs/float": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
- "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E="
- },
- "@protobufjs/inquire": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
- "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik="
- },
- "@protobufjs/path": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
- "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0="
- },
- "@protobufjs/pool": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
- "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q="
- },
- "@protobufjs/utf8": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
- "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
- },
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@@ -106,11 +52,6 @@
"@types/node": "*"
}
},
- "@types/long": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz",
- "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q=="
- },
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -120,7 +61,8 @@
"@types/node": {
"version": "10.12.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.10.tgz",
- "integrity": "sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w=="
+ "integrity": "sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w==",
+ "dev": true
},
"abbrev": {
"version": "1.1.1",
@@ -182,11 +124,6 @@
}
}
},
- "ansi-regex": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
- "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
- },
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -206,22 +143,6 @@
"normalize-path": "^2.1.1"
}
},
- "aproba": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
- "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
- "optional": true
- },
- "are-we-there-yet": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
- "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
- "optional": true,
- "requires": {
- "delegates": "^1.0.0",
- "readable-stream": "^2.0.6"
- }
- },
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -331,7 +252,8 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
},
"base": {
"version": "0.11.2",
@@ -397,39 +319,12 @@
"tweetnacl": "^0.14.3"
}
},
- "binary": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
- "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
- "requires": {
- "buffers": "~0.1.1",
- "chainsaw": "~0.1.0"
- }
- },
"binary-extensions": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
"integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
"dev": true
},
- "bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "optional": true,
- "requires": {
- "file-uri-to-path": "1.0.0"
- }
- },
- "bl": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz",
- "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==",
- "requires": {
- "readable-stream": "^2.3.5",
- "safe-buffer": "^5.1.1"
- }
- },
"boxen": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
@@ -482,6 +377,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -516,53 +412,6 @@
}
}
},
- "buffer-alloc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
- "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
- "optional": true,
- "requires": {
- "buffer-alloc-unsafe": "^1.1.0",
- "buffer-fill": "^1.0.0"
- }
- },
- "buffer-alloc-unsafe": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
- "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
- "optional": true
- },
- "buffer-crc32": {
- "version": "0.2.13",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
- "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
- },
- "buffer-fill": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
- "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
- "optional": true
- },
- "buffermaker": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/buffermaker/-/buffermaker-1.2.1.tgz",
- "integrity": "sha512-IdnyU2jDHU65U63JuVQNTHiWjPRH0CS3aYd/WPaEwyX84rFdukhOduAVb1jwUScmb5X0JWPw8NZOrhoLMiyAHQ==",
- "requires": {
- "long": "1.1.2"
- },
- "dependencies": {
- "long": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/long/-/long-1.1.2.tgz",
- "integrity": "sha1-6u9ZUcp1UdlpJrgtokLbnWso+1M="
- }
- }
- },
- "buffers": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
- "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
- },
"byline": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
@@ -610,14 +459,6 @@
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
"dev": true
},
- "chainsaw": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
- "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
- "requires": {
- "traverse": ">=0.3.0 <0.4"
- }
- },
"chalk": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
@@ -650,12 +491,6 @@
"upath": "^1.0.5"
}
},
- "chownr": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz",
- "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==",
- "optional": true
- },
"ci-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
@@ -691,11 +526,6 @@
"integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=",
"dev": true
},
- "code-point-at": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
- "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
- },
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -774,7 +604,8 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
},
"config": {
"version": "3.2.2",
@@ -798,11 +629,6 @@
"xdg-basedir": "^3.0.0"
}
},
- "console-control-strings": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
- "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
- },
"copy-descriptor": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
@@ -858,6 +684,7 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -868,19 +695,11 @@
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
- "decompress-response": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
- "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
- "optional": true,
- "requires": {
- "mimic-response": "^1.0.0"
- }
- },
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "dev": true
},
"deep-is": {
"version": "0.1.3",
@@ -935,23 +754,6 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
- "delegates": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
- "optional": true
- },
- "denque": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
- "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
- },
- "detect-libc": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
- "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
- "optional": true
- },
"diagnostics": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
@@ -1004,14 +806,6 @@
"env-variable": "0.0.x"
}
},
- "end-of-stream": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
- "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
- "requires": {
- "once": "^1.4.0"
- }
- },
"env-variable": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz",
@@ -1272,12 +1066,6 @@
"moment": "^2.11.2"
}
},
- "file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "optional": true
- },
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -1343,12 +1131,6 @@
"readable-stream": "^2.0.0"
}
},
- "fs-constants": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "optional": true
- },
"fs-extra": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
@@ -1895,22 +1677,6 @@
}
}
},
- "gauge": {
- "version": "2.7.4",
- "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
- "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
- "optional": true,
- "requires": {
- "aproba": "^1.0.3",
- "console-control-strings": "^1.0.0",
- "has-unicode": "^2.0.0",
- "object-assign": "^4.1.0",
- "signal-exit": "^3.0.0",
- "string-width": "^1.0.1",
- "strip-ansi": "^3.0.1",
- "wide-align": "^1.1.0"
- }
- },
"get-stream": {
"version": "3.0.0",
"resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
@@ -1932,12 +1698,6 @@
"assert-plus": "^1.0.0"
}
},
- "github-from-package": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=",
- "optional": true
- },
"glob": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
@@ -2059,12 +1819,6 @@
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
- "has-unicode": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
- "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
- "optional": true
- },
"has-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@@ -2150,7 +1904,8 @@
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
- "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
+ "dev": true
},
"into-stream": {
"version": "5.1.0",
@@ -2262,14 +2017,6 @@
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
- "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
- "requires": {
- "number-is-nan": "^1.0.0"
- }
- },
"is-glob": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
@@ -2453,42 +2200,12 @@
"verror": "1.10.0"
}
},
- "kafka-node": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/kafka-node/-/kafka-node-4.1.3.tgz",
- "integrity": "sha512-C2WHksRCr7vIKmbxYaCk2c5Q1lnHIi6C0f3AioK3ARcRHGO9DpqErcoaS9d8PP62yzTnkYras+iAlmPsZHNSfw==",
- "requires": {
- "async": "^2.6.2",
- "binary": "~0.3.0",
- "bl": "^2.2.0",
- "buffer-crc32": "~0.2.5",
- "buffermaker": "~1.2.0",
- "debug": "^2.1.3",
- "denque": "^1.3.0",
- "lodash": "^4.17.4",
- "minimatch": "^3.0.2",
- "nested-error-stacks": "^2.0.0",
- "optional": "^0.1.3",
- "retry": "^0.10.1",
- "snappy": "^6.0.1",
- "uuid": "^3.0.0"
- },
- "dependencies": {
- "async": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
- "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
- "requires": {
- "lodash": "^4.17.14"
- },
- "dependencies": {
- "lodash": {
- "version": "4.17.15",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
- }
- }
- }
+ "kafkajs": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-1.11.0.tgz",
+ "integrity": "sha512-dLRCcFIBygZucR+e8U2ZqH2wgMrAu114K0szUyUseJoeOii3cG5bHZPIdqKecXxI6begPVCfGS3R0nJY4zHW2A==",
+ "requires": {
+ "long": "^4.0.0"
}
},
"kind-of": {
@@ -2641,16 +2358,11 @@
"mime-db": "1.40.0"
}
},
- "mimic-response": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
- "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
- "optional": true
- },
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -2685,6 +2397,7 @@
"version": "0.5.1",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
"requires": {
"minimist": "0.0.8"
},
@@ -2692,7 +2405,8 @@
"minimist": {
"version": "0.0.8",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
- "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
}
}
},
@@ -2704,7 +2418,8 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "dev": true
},
"multistream": {
"version": "2.1.1",
@@ -2742,26 +2457,6 @@
"to-regex": "^3.0.1"
}
},
- "napi-build-utils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz",
- "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==",
- "optional": true
- },
- "nested-error-stacks": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz",
- "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug=="
- },
- "node-abi": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.9.0.tgz",
- "integrity": "sha512-jmEOvv0eanWjhX8dX1pmjb7oJl1U1oR4FOh0b2GnvALwSYoOdU7sj+kLDSAyjo4pfC9aj/IxkloxdLJQhSSQBA==",
- "optional": true,
- "requires": {
- "semver": "^5.4.1"
- }
- },
"nodemon": {
"version": "1.18.7",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.7.tgz",
@@ -2797,12 +2492,6 @@
}
}
},
- "noop-logger": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
- "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=",
- "optional": true
- },
"nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
@@ -2830,35 +2519,12 @@
"path-key": "^2.0.0"
}
},
- "npmlog": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
- "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
- "optional": true,
- "requires": {
- "are-we-there-yet": "~1.1.2",
- "console-control-strings": "~1.1.0",
- "gauge": "~2.7.3",
- "set-blocking": "~2.0.0"
- }
- },
- "number-is-nan": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
- "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
- },
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"dev": true
},
- "object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "optional": true
- },
"object-copy": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
@@ -2917,6 +2583,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
"requires": {
"wrappy": "1"
}
@@ -2926,11 +2593,6 @@
"resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz",
"integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4="
},
- "optional": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz",
- "integrity": "sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw=="
- },
"optionator": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
@@ -2945,12 +2607,6 @@
"wordwrap": "~1.0.0"
}
},
- "os-homedir": {
- "version": "1.0.2",
- "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
- "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
- "optional": true
- },
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@@ -3127,38 +2783,6 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true
},
- "prebuild-install": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.0.tgz",
- "integrity": "sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==",
- "optional": true,
- "requires": {
- "detect-libc": "^1.0.3",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.0",
- "mkdirp": "^0.5.1",
- "napi-build-utils": "^1.0.1",
- "node-abi": "^2.7.0",
- "noop-logger": "^0.1.1",
- "npmlog": "^4.0.1",
- "os-homedir": "^1.0.1",
- "pump": "^2.0.1",
- "rc": "^1.2.7",
- "simple-get": "^2.7.0",
- "tar-fs": "^1.13.0",
- "tunnel-agent": "^0.6.0",
- "which-pm-runs": "^1.0.0"
- },
- "dependencies": {
- "expand-template": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "optional": true
- }
- }
- },
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -3182,26 +2806,6 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
- "protobufjs": {
- "version": "6.8.8",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
- "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==",
- "requires": {
- "@protobufjs/aspromise": "^1.1.2",
- "@protobufjs/base64": "^1.1.2",
- "@protobufjs/codegen": "^2.0.4",
- "@protobufjs/eventemitter": "^1.1.0",
- "@protobufjs/fetch": "^1.1.0",
- "@protobufjs/float": "^1.0.2",
- "@protobufjs/inquire": "^1.1.0",
- "@protobufjs/path": "^1.1.2",
- "@protobufjs/pool": "^1.1.0",
- "@protobufjs/utf8": "^1.1.0",
- "@types/long": "^4.0.0",
- "@types/node": "^10.1.0",
- "long": "^4.0.0"
- }
- },
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@@ -3220,16 +2824,6 @@
"integrity": "sha512-vL6NLxNHzkNTjGJUpMm5PLC+94/0tTlC1vkP9bdU0pOHih+EujMjgMTwfZopZvHWRFbqJ5Y73OMoau50PewDDA==",
"dev": true
},
- "pump": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
- "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
- "optional": true,
- "requires": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -3246,6 +2840,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dev": true,
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
@@ -3389,11 +2984,6 @@
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
"dev": true
},
- "retry": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
- "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q="
- },
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -3428,12 +3018,6 @@
"semver": "^5.0.3"
}
},
- "set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
- "optional": true
- },
"set-value": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
@@ -3475,24 +3059,8 @@
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
- "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
- },
- "simple-concat": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
- "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=",
- "optional": true
- },
- "simple-get": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz",
- "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==",
- "optional": true,
- "requires": {
- "decompress-response": "^3.3.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "dev": true
},
"simple-swizzle": {
"version": "0.2.2",
@@ -3615,25 +3183,6 @@
}
}
},
- "snappy": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/snappy/-/snappy-6.2.3.tgz",
- "integrity": "sha512-HZpVoIxMfQ4fL3iDuMdI1R5xycw1o9YDCAndTKZCY/EHRoKFvzwplttuBBVGeEg2fd1hYiwAXos/sM24W7N1LA==",
- "optional": true,
- "requires": {
- "bindings": "^1.3.1",
- "nan": "^2.14.0",
- "prebuild-install": "^5.2.2"
- },
- "dependencies": {
- "nan": {
- "version": "2.14.0",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
- "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
- "optional": true
- }
- }
- },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -3725,16 +3274,6 @@
"readable-stream": "^2.1.4"
}
},
- "string-width": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
- "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
- "requires": {
- "code-point-at": "^1.0.0",
- "is-fullwidth-code-point": "^1.0.0",
- "strip-ansi": "^3.0.0"
- }
- },
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -3743,14 +3282,6 @@
"safe-buffer": "~5.1.0"
}
},
- "strip-ansi": {
- "version": "3.0.1",
- "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
- "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- },
"strip-eof": {
"version": "1.0.0",
"resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
@@ -3760,7 +3291,8 @@
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true
},
"supports-color": {
"version": "5.5.0",
@@ -3771,57 +3303,6 @@
"has-flag": "^3.0.0"
}
},
- "tar-fs": {
- "version": "1.16.3",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz",
- "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==",
- "optional": true,
- "requires": {
- "chownr": "^1.0.1",
- "mkdirp": "^0.5.1",
- "pump": "^1.0.0",
- "tar-stream": "^1.1.2"
- },
- "dependencies": {
- "pump": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz",
- "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==",
- "optional": true,
- "requires": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- }
- }
- },
- "tar-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
- "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
- "optional": true,
- "requires": {
- "bl": "^1.0.0",
- "buffer-alloc": "^1.2.0",
- "end-of-stream": "^1.0.0",
- "fs-constants": "^1.0.0",
- "readable-stream": "^2.3.0",
- "to-buffer": "^1.1.1",
- "xtend": "^4.0.0"
- },
- "dependencies": {
- "bl": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
- "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
- "optional": true,
- "requires": {
- "readable-stream": "^2.3.5",
- "safe-buffer": "^5.1.1"
- }
- }
- }
- },
"term-size": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
@@ -3848,12 +3329,6 @@
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
"dev": true
},
- "to-buffer": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
- "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==",
- "optional": true
- },
"to-object-path": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
@@ -3923,11 +3398,6 @@
}
}
},
- "traverse": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
- "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
- },
"triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
@@ -3937,6 +3407,7 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "dev": true,
"requires": {
"safe-buffer": "^5.0.1"
}
@@ -4140,7 +3611,8 @@
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
- "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
+ "dev": true
},
"uuid-parse": {
"version": "1.0.0",
@@ -4167,21 +3639,6 @@
"isexe": "^2.0.0"
}
},
- "which-pm-runs": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
- "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=",
- "optional": true
- },
- "wide-align": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
- "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
- "optional": true,
- "requires": {
- "string-width": "^1.0.2 || 2"
- }
- },
"widest-line": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz",
@@ -4281,7 +3738,8 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
},
"write-file-atomic": {
"version": "2.3.0",
@@ -4300,12 +3758,6 @@
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
"dev": true
},
- "xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "optional": true
- },
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json
index d61162dd5b..da4eb09b09 100644
--- a/msa/js-executor/package.json
+++ b/msa/js-executor/package.json
@@ -1,23 +1,21 @@
{
"name": "thingsboard-js-executor",
"private": true,
- "version": "2.4.1",
+ "version": "2.4.2",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.js",
"bin": "server.js",
"scripts": {
- "build-proto": "pbjs -t static-module -w commonjs -o ./api/jsinvoke.proto.js ../../application/src/main/proto/jsinvoke.proto",
- "install": "npm run build-proto && pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js",
+ "install": "pkg -t node10-linux-x64,node10-win-x64 --out-path ./target . && node install.js",
"test": "echo \"Error: no test specified\" && exit 1",
- "start": "npm run build-proto && nodemon server.js",
- "start-prod": "npm run build-proto && NODE_ENV=production nodemon server.js"
+ "start": "nodemon server.js",
+ "start-prod": "NODE_ENV=production nodemon server.js"
},
"dependencies": {
"config": "^3.2.2",
"js-yaml": "^3.12.0",
- "kafka-node": "^4.1.3",
+ "kafkajs": "^1.11.0",
"long": "^4.0.0",
- "protobufjs": "^6.8.8",
"uuid-parse": "^1.0.0",
"winston": "^3.0.0",
"winston-daily-rotate-file": "^3.2.1"
diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml
index 27636dbecd..53616e6493 100644
--- a/msa/js-executor/pom.xml
+++ b/msa/js-executor/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
msa
org.thingsboard.msa
diff --git a/msa/js-executor/server.js b/msa/js-executor/server.js
index 8a0912bcc2..6f322f0892 100644
--- a/msa/js-executor/server.js
+++ b/msa/js-executor/server.js
@@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+const { logLevel, Kafka } = require('kafkajs');
+
const config = require('config'),
- kafka = require('kafka-node'),
- ConsumerGroup = kafka.ConsumerGroup,
- Producer = kafka.Producer,
JsInvokeMessageProcessor = require('./api/jsInvokeMessageProcessor'),
- logger = require('./config/logger')('main');
+ logger = require('./config/logger')._logger('main'),
+ KafkaJsWinstonLogCreator = require('./config/logger').KafkaJsWinstonLogCreator;
var kafkaClient;
+var consumer;
+var producer;
(async() => {
try {
@@ -32,49 +35,24 @@ var kafkaClient;
logger.info('Kafka Bootstrap Servers: %s', kafkaBootstrapServers);
logger.info('Kafka Requests Topic: %s', kafkaRequestTopic);
- kafkaClient = new kafka.KafkaClient({kafkaHost: kafkaBootstrapServers});
-
- var consumer = new ConsumerGroup(
- {
- kafkaHost: kafkaBootstrapServers,
- groupId: 'js-executor-group',
- autoCommit: true,
- encoding: 'buffer'
- },
- kafkaRequestTopic
- );
-
- consumer.on('error', (err) => {
- logger.error('Unexpected kafka consumer error: %s', err.message);
- logger.error(err.stack);
- });
-
- consumer.on('offsetOutOfRange', (err) => {
- logger.error('Offset out of range error: %s', err.message);
- logger.error(err.stack);
- });
-
- consumer.on('rebalancing', () => {
- logger.info('Rebalancing event received.');
- })
-
- consumer.on('rebalanced', () => {
- logger.info('Rebalanced event received.');
- });
-
- var producer = new Producer(kafkaClient);
- producer.on('error', (err) => {
- logger.error('Unexpected kafka producer error: %s', err.message);
- logger.error(err.stack);
+ kafkaClient = new Kafka({
+ brokers: kafkaBootstrapServers.split(','),
+ logLevel: logLevel.INFO,
+ logCreator: KafkaJsWinstonLogCreator
});
- var messageProcessor = new JsInvokeMessageProcessor(producer);
+ consumer = kafkaClient.consumer({ groupId: 'js-executor-group' });
+ producer = kafkaClient.producer();
+ const messageProcessor = new JsInvokeMessageProcessor(producer);
+ await consumer.connect();
+ await producer.connect();
+ await consumer.subscribe({ topic: kafkaRequestTopic});
- producer.on('ready', () => {
- consumer.on('message', (message) => {
+ logger.info('Started ThingsBoard JavaScript Executor Microservice.');
+ await consumer.run({
+ eachMessage: async ({ topic, partition, message }) => {
messageProcessor.onJsInvokeMessage(message);
- });
- logger.info('Started ThingsBoard JavaScript Executor Microservice.');
+ },
});
} catch (e) {
@@ -84,21 +62,41 @@ var kafkaClient;
}
})();
-process.on('exit', function () {
+process.on('exit', () => {
exit(0);
});
-function exit(status) {
+async function exit(status) {
logger.info('Exiting with status: %d ...', status);
- if (kafkaClient) {
- logger.info('Stopping Kafka Client...');
- var _kafkaClient = kafkaClient;
- kafkaClient = null;
- _kafkaClient.close(() => {
- logger.info('Kafka Client stopped.');
+ if (consumer) {
+ logger.info('Stopping Kafka Consumer...');
+ var _consumer = consumer;
+ consumer = null;
+ try {
+ await _consumer.disconnect();
+ logger.info('Kafka Consumer stopped.');
+ await disconnectProducer();
process.exit(status);
- });
+ } catch (e) {
+ logger.info('Kafka Consumer stop error.');
+ await disconnectProducer();
+ process.exit(status);
+ }
} else {
process.exit(status);
}
}
+
+async function disconnectProducer() {
+ if (producer) {
+ logger.info('Stopping Kafka Producer...');
+ var _producer = producer;
+ producer = null;
+ try {
+ await _producer.disconnect();
+ logger.info('Kafka Producer stopped.');
+ } catch (e) {
+ logger.info('Kafka Producer stop error.');
+ }
+ }
+}
diff --git a/msa/pom.xml b/msa/pom.xml
index 84d00e2d88..5ea89c92f9 100644
--- a/msa/pom.xml
+++ b/msa/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
thingsboard
msa
diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml
index 8a2bab084a..20f013a13a 100644
--- a/msa/tb-node/pom.xml
+++ b/msa/tb-node/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
msa
org.thingsboard.msa
diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml
index 680d39ede7..d0e78b334a 100644
--- a/msa/tb/pom.xml
+++ b/msa/tb/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
msa
org.thingsboard.msa
@@ -40,7 +40,7 @@
tb-cassandra
thingsboard
/usr/share/${pkg.name}
- 2.4.0
+ 2.4.1
diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml
index b606ba29fd..99c7d64423 100644
--- a/msa/transport/coap/pom.xml
+++ b/msa/transport/coap/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 2.4.2-SNAPSHOT
+ 2.4.2
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml
index c38756d5cd..83ec9f5d94 100644
--- a/msa/transport/http/pom.xml
+++ b/msa/transport/http/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 2.4.2-SNAPSHOT
+ 2.4.2
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml
index 7402d2e209..aab05438f3 100644
--- a/msa/transport/mqtt/pom.xml
+++ b/msa/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard.msa
- 2.4.2-SNAPSHOT
+ 2.4.2
transport
org.thingsboard.msa.transport
diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml
index f869e02f18..5a6defbb89 100644
--- a/msa/transport/pom.xml
+++ b/msa/transport/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
msa
org.thingsboard.msa
diff --git a/msa/web-ui/package-lock.json b/msa/web-ui/package-lock.json
index 9298d31f27..3ceb92e809 100644
--- a/msa/web-ui/package-lock.json
+++ b/msa/web-ui/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "thingsboard-web-ui",
- "version": "2.4.1",
+ "version": "2.4.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json
index 2d36752300..ce82272a26 100644
--- a/msa/web-ui/package.json
+++ b/msa/web-ui/package.json
@@ -1,7 +1,7 @@
{
"name": "thingsboard-web-ui",
"private": true,
- "version": "2.4.1",
+ "version": "2.4.2",
"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 c7bcf39caf..a55d816c33 100644
--- a/msa/web-ui/pom.xml
+++ b/msa/web-ui/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
msa
org.thingsboard.msa
diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml
index 869eb2abb0..349a154ed3 100644
--- a/netty-mqtt/pom.xml
+++ b/netty-mqtt/pom.xml
@@ -19,12 +19,12 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
thingsboard
org.thingsboard
netty-mqtt
- 2.4.2-SNAPSHOT
+ 2.4.2
jar
Netty MQTT Client
diff --git a/pom.xml b/pom.xml
index f0a0aae6cd..8dd4484e44 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
pom
Thingsboard
@@ -81,7 +81,7 @@
2.5.0
2.5.3
1.2.1
- 9.4.1211
+ 9.4.1212
org/thingsboard/server/gen/**/*,
org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
index 4197f275b6..f40fce22a6 100644
--- a/rule-engine/pom.xml
+++ b/rule-engine/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
thingsboard
rule-engine
diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml
index 02e4d128dc..a24bebf8b1 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
- 2.4.2-SNAPSHOT
+ 2.4.2
rule-engine
org.thingsboard.rule-engine
@@ -48,6 +48,11 @@
dao-api
provided
+
+ org.thingsboard.common
+ util
+ provided
+
io.netty
netty-all
@@ -83,5 +88,10 @@
cassandra-driver-extras
provided
+
+ org.springframework.data
+ spring-data-redis
+ provided
+
-
\ No newline at end of file
+
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
index 4d9a4e612b..f72adf04ab 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
@@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.api;
import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.msg.TbMsg;
import javax.script.ScriptException;
@@ -25,10 +26,14 @@ public interface ScriptEngine {
TbMsg executeUpdate(TbMsg msg) throws ScriptException;
+ ListenableFuture executeUpdateAsync(TbMsg msg);
+
TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException;
boolean executeFilter(TbMsg msg) throws ScriptException;
+ ListenableFuture executeFilterAsync(TbMsg msg);
+
Set executeSwitch(TbMsg msg) throws ScriptException;
JsonNode executeJson(TbMsg msg) throws ScriptException;
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 034aec3000..d91f12da96 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
@@ -15,7 +15,10 @@
*/
package org.thingsboard.rule.engine.api;
+import com.datastax.driver.core.ResultSetFuture;
import io.netty.channel.EventLoopGroup;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
@@ -41,8 +44,6 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
-import com.datastax.driver.core.ResultSetFuture;
-
import java.util.Set;
/**
@@ -122,6 +123,12 @@ public interface TbContext {
ScriptEngine createJsScriptEngine(String script, String... argNames);
+ void logJsEvalRequest();
+
+ void logJsEvalResponse();
+
+ void logJsEvalFailure();
+
String getNodeId();
RuleChainTransactionService getRuleChainTransactionService();
@@ -132,4 +139,7 @@ public interface TbContext {
ResultSetFuture submitCassandraTask(CassandraStatementTask task);
+ RedisTemplate getRedisTemplate();
+
+ String getServerAddress();
}
diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index 627aa42ab3..69960df3b0 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
- 2.4.2-SNAPSHOT
+ 2.4.2
rule-engine
org.thingsboard.rule-engine
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
index f295187170..0a7f6113f1 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
@@ -65,15 +65,22 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
+ ctx.logJsEvalRequest();
ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails());
return Futures.transformAsync(asyncDetails, details -> {
+ ctx.logJsEvalResponse();
ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), details, System.currentTimeMillis());
return Futures.transformAsync(clearFuture, cleared -> {
- if (cleared && details != null) {
- alarm.setDetails(details);
- }
- alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
- return Futures.immediateFuture(new AlarmResult(false, false, true, alarm));
+ ListenableFuture savedAlarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), alarm.getId());
+ return Futures.transformAsync(savedAlarmFuture, savedAlarm -> {
+ if (cleared && savedAlarm != null) {
+ alarm.setDetails(savedAlarm.getDetails());
+ alarm.setEndTs(savedAlarm.getEndTs());
+ alarm.setClearTs(savedAlarm.getClearTs());
+ }
+ alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
+ return Futures.immediateFuture(new AlarmResult(false, false, true, alarm));
+ });
});
}, ctx.getDbCallbackExecutor());
}
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 dcf7b6987d..d6f6236311 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
@@ -42,10 +42,10 @@ import java.io.IOException;
nodeDescription = "Create or Update Alarm",
nodeDetails =
"Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
- "Node output:\n" +
- "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm'. " +
- "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " +
- "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.",
+ "Node output:\n" +
+ "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm'. " +
+ "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " +
+ "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeCreateAlarmConfig",
icon = "notifications_active"
@@ -103,11 +103,15 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) {
ListenableFuture asyncAlarm;
- if (msgAlarm != null ) {
+ if (msgAlarm != null) {
asyncAlarm = Futures.immediateCheckedFuture(msgAlarm);
} else {
+ ctx.logJsEvalRequest();
asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
- details -> buildAlarm(msg, details, ctx.getTenantId()));
+ details -> {
+ ctx.logJsEvalResponse();
+ return buildAlarm(msg, details, ctx.getTenantId());
+ });
}
ListenableFuture asyncCreated = Futures.transform(asyncAlarm,
alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
@@ -115,7 +119,9 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) {
+ ctx.logJsEvalRequest();
ListenableFuture asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function) details -> {
+ ctx.logJsEvalResponse();
if (msgAlarm != null) {
existingAlarm.setSeverity(msgAlarm.getSeverity());
existingAlarm.setPropagate(msgAlarm.isPropagate());
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
index 7a1578c0a1..f3fd84788c 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
@@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.action;
import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType;
@@ -52,12 +53,17 @@ public class TbLogNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ ctx.logJsEvalRequest();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),
toString -> {
+ ctx.logJsEvalResponse();
log.info(toString);
ctx.tellNext(msg, SUCCESS);
},
- t -> ctx.tellFailure(msg, t));
+ t -> {
+ ctx.logJsEvalResponse();
+ ctx.tellFailure(msg, t);
+ });
}
@Override
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 478b0b4666..d5ac552595 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
@@ -129,7 +129,9 @@ public class TbMsgGeneratorNode implements TbNode {
prevMsg = ctx.newMsg("", originatorId, new TbMsgMetaData(), "{}");
}
if (initialized) {
+ ctx.logJsEvalRequest();
TbMsg generated = jsEngine.executeGenerate(prevMsg);
+ ctx.logJsEvalResponse();
prevMsg = ctx.newMsg(generated.getType(), originatorId, generated.getMetaData(), generated.getData());
}
return prevMsg;
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
index afcf9dd80f..2effb714b9 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
@@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType;
@@ -50,10 +51,16 @@ public class TbJsFilterNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
- ListeningExecutor jsExecutor = ctx.getJsExecutor();
- withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)),
- filterResult -> ctx.tellNext(msg, filterResult ? "True" : "False"),
- t -> ctx.tellFailure(msg, t));
+ ctx.logJsEvalRequest();
+ withCallback(jsEngine.executeFilterAsync(msg),
+ filterResult -> {
+ ctx.logJsEvalResponse();
+ ctx.tellNext(msg, filterResult ? "True" : "False");
+ },
+ t -> {
+ ctx.tellFailure(msg, t);
+ ctx.logJsEvalFailure();
+ }, ctx.getDbCallbackExecutor());
}
@Override
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
index d8e25387b9..5ba4d2b0dd 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
@@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.filter;
import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType;
@@ -53,9 +54,16 @@ public class TbJsSwitchNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
+ ctx.logJsEvalRequest();
withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)),
- result -> processSwitch(ctx, msg, result),
- t -> ctx.tellFailure(msg, t));
+ result -> {
+ ctx.logJsEvalResponse();
+ processSwitch(ctx, msg, result);
+ },
+ t -> {
+ ctx.logJsEvalFailure();
+ ctx.tellFailure(msg, t);
+ }, ctx.getDbCallbackExecutor());
}
private void processSwitch(TbContext ctx, TbMsg msg, Set nextRelations) {
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 df6bcf9f58..7a26faa8f4 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
@@ -15,6 +15,10 @@
*/
package org.thingsboard.rule.engine.metadata;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+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.apache.commons.collections.CollectionUtils;
@@ -40,11 +44,18 @@ import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
public abstract class TbAbstractGetAttributesNode implements TbNode {
+ private static ObjectMapper mapper = new ObjectMapper();
+
+ private static final String VALUE = "value";
+ private static final String TS = "ts";
+
protected C config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadGetAttributesNodeConfig(configuration);
+ mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
+ mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
}
protected abstract C loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;
@@ -61,6 +72,11 @@ public abstract class TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg);
+
private void safePutAttributes(TbContext ctx, TbMsg msg, T entityId) {
if (entityId == null || entityId.isNullUid()) {
ctx.tellNext(msg, FAILURE);
@@ -106,15 +122,22 @@ public abstract class TbAbstractGetAttributesNode> latest = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, keys);
return Futures.transform(latest, l -> {
l.forEach(r -> {
+ boolean getLatestValueWithTs = BooleanUtils.toBooleanDefaultIfNull(this.config.isGetLatestValueWithTs(), false);
if (BooleanUtils.toBooleanDefaultIfNull(this.config.isTellFailureIfAbsent(), true)) {
- if (r.getValue() != null) {
- msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
- } else {
+ if (r.getValue() == null) {
throw new RuntimeException("[" + r.getKey() + "] telemetry value is not present in the DB!");
+ } else if (getLatestValueWithTs) {
+ putValueWithTs(msg, r);
+ } else {
+ msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
}
} else {
if (r.getValue() != null) {
- msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
+ if (getLatestValueWithTs) {
+ putValueWithTs(msg, r);
+ } else {
+ msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
+ }
}
}
});
@@ -122,10 +145,23 @@ public abstract class TbAbstractGetAttributesNode findEntityIdAsync(TbContext ctx, TbMsg msg);
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
index 2b47b5264a..a9a5c47ba0 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
@@ -34,6 +34,7 @@ public class TbGetAttributesNodeConfiguration implements NodeConfiguration latestTsKeyNames;
private boolean tellFailureIfAbsent;
+ private boolean getLatestValueWithTs;
@Override
public TbGetAttributesNodeConfiguration defaultConfiguration() {
@@ -43,6 +44,7 @@ public class TbGetAttributesNodeConfiguration implements NodeConfiguration entries, TbMsg msg) {
ObjectNode resultNode = mapper.createObjectNode();
- if (limit == MAX_FETCH_SIZE) {
+ if (FETCH_MODE_ALL.equals(fetchMode)) {
entries.forEach(entry -> processArray(resultNode, entry));
} else {
entries.forEach(entry -> processSingle(resultNode, entry));
@@ -156,12 +156,10 @@ public class TbGetTelemetryNode implements TbNode {
private void processArray(ObjectNode node, TsKvEntry entry) {
if (node.has(entry.getKey())) {
ArrayNode arrayNode = (ArrayNode) node.get(entry.getKey());
- ObjectNode obj = buildNode(entry);
- arrayNode.add(obj);
+ arrayNode.add(buildNode(entry));
} else {
ArrayNode arrayNode = mapper.createArrayNode();
- ObjectNode obj = buildNode(entry);
- arrayNode.add(obj);
+ arrayNode.add(buildNode(entry));
node.set(entry.getKey(), arrayNode);
}
}
@@ -254,6 +252,14 @@ public class TbGetTelemetryNode implements TbNode {
return pattern.replaceAll("[${}]", "");
}
+ private int validateLimit(int limit) {
+ if (limit != 0) {
+ return limit;
+ } else {
+ return MAX_FETCH_SIZE;
+ }
+ }
+
@Data
@NoArgsConstructor
private static class Interval {
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java
index 05335879ca..7a312c34fc 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java
@@ -45,7 +45,8 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration latestTsKeyNames;
@@ -62,6 +63,7 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration>> pendingFutures;
+
+ TbHttpClient(TbRestApiCallNodeConfiguration config) throws TbNodeException {
+ try {
+ this.config = config;
+ if (config.getMaxParallelRequestsCount() > 0) {
+ pendingFutures = new ConcurrentLinkedDeque<>();
+ }
+ if (config.isUseSimpleClientHttpFactory()) {
+ httpClient = new AsyncRestTemplate();
+ } else {
+ this.eventLoopGroup = new NioEventLoopGroup();
+ Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup);
+ nettyFactory.setSslContext(SslContextBuilder.forClient().build());
+ nettyFactory.setReadTimeout(config.getReadTimeoutMs());
+ httpClient = new AsyncRestTemplate(nettyFactory);
+ }
+ } catch (SSLException e) {
+ throw new TbNodeException(e);
+ }
+ }
+
+ void destroy() {
+ if (this.eventLoopGroup != null) {
+ this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+ }
+ }
+
+ void processMessage(TbContext ctx, TbMsg msg, TbRedisQueueProcessor queueProcessor) {
+ String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData());
+ HttpHeaders headers = prepareHeaders(msg.getMetaData());
+ HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
+ HttpEntity entity = new HttpEntity<>(msg.getData(), headers);
+
+ ListenableFuture> future = httpClient.exchange(
+ endpointUrl, method, entity, String.class);
+ future.addCallback(new ListenableFutureCallback>() {
+ @Override
+ public void onFailure(Throwable throwable) {
+ if (config.isUseRedisQueueForMsgPersistence()) {
+ if (throwable instanceof HttpClientErrorException) {
+ processHttpClientError(((HttpClientErrorException) throwable).getStatusCode(), msg, queueProcessor);
+ } else {
+ queueProcessor.pushOnFailure(msg);
+ }
+ }
+ TbMsg next = processException(ctx, msg, throwable);
+ ctx.tellFailure(next, throwable);
+ }
+
+ @Override
+ public void onSuccess(ResponseEntity responseEntity) {
+ if (responseEntity.getStatusCode().is2xxSuccessful()) {
+ if (config.isUseRedisQueueForMsgPersistence()) {
+ queueProcessor.resetCounter();
+ }
+ TbMsg next = processResponse(ctx, msg, responseEntity);
+ ctx.tellNext(next, TbRelationTypes.SUCCESS);
+ } else {
+ if (config.isUseRedisQueueForMsgPersistence()) {
+ processHttpClientError(responseEntity.getStatusCode(), msg, queueProcessor);
+ }
+ TbMsg next = processFailureResponse(ctx, msg, responseEntity);
+ ctx.tellNext(next, TbRelationTypes.FAILURE);
+ }
+ }
+ });
+ if (pendingFutures != null) {
+ processParallelRequests(future);
+ }
+ }
+
+ private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) {
+ TbMsgMetaData metaData = origMsg.getMetaData();
+ metaData.putValue(STATUS, response.getStatusCode().name());
+ metaData.putValue(STATUS_CODE, response.getStatusCode().value() + "");
+ metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
+ response.getHeaders().toSingleValueMap().forEach(metaData::putValue);
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, response.getBody());
+ }
+
+ private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) {
+ TbMsgMetaData metaData = origMsg.getMetaData();
+ metaData.putValue(STATUS, response.getStatusCode().name());
+ metaData.putValue(STATUS_CODE, response.getStatusCode().value() + "");
+ metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
+ metaData.putValue(ERROR_BODY, response.getBody());
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {
+ TbMsgMetaData metaData = origMsg.getMetaData();
+ metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
+ if (e instanceof HttpClientErrorException) {
+ HttpClientErrorException httpClientErrorException = (HttpClientErrorException) e;
+ metaData.putValue(STATUS, httpClientErrorException.getStatusText());
+ metaData.putValue(STATUS_CODE, httpClientErrorException.getRawStatusCode() + "");
+ metaData.putValue(ERROR_BODY, httpClientErrorException.getResponseBodyAsString());
+ }
+ return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
+ }
+
+ private HttpHeaders prepareHeaders(TbMsgMetaData metaData) {
+ HttpHeaders headers = new HttpHeaders();
+ config.getHeaders().forEach((k, v) -> headers.add(TbNodeUtils.processPattern(k, metaData), TbNodeUtils.processPattern(v, metaData)));
+ return headers;
+ }
+
+ private void processParallelRequests(ListenableFuture> future) {
+ pendingFutures.add(future);
+ if (pendingFutures.size() > config.getMaxParallelRequestsCount()) {
+ for (int i = 0; i < config.getMaxParallelRequestsCount(); i++) {
+ try {
+ ListenableFuture> pendingFuture = pendingFutures.removeFirst();
+ try {
+ pendingFuture.get(config.getReadTimeoutMs(), TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ log.warn("Timeout during waiting for reply!", e);
+ pendingFuture.cancel(true);
+ }
+ } catch (Exception e) {
+ log.warn("Failure during waiting for reply!", e);
+ }
+ }
+ }
+ }
+
+ private void processHttpClientError(HttpStatus statusCode, TbMsg msg, TbRedisQueueProcessor queueProcessor) {
+ if (statusCode.is4xxClientError()) {
+ log.warn("[{}] Client error during message delivering!", msg);
+ } else {
+ queueProcessor.pushOnFailure(msg);
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRedisQueueProcessor.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRedisQueueProcessor.java
new file mode 100644
index 0000000000..7e10b469f2
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRedisQueueProcessor.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright © 2016-2019 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.rest;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.ListOperations;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Data
+@Slf4j
+class TbRedisQueueProcessor {
+
+ private static final int MAX_QUEUE_SIZE = Integer.MAX_VALUE;
+
+ private final TbContext ctx;
+ private final TbHttpClient httpClient;
+ private final ExecutorService executor;
+ private final ListOperations listOperations;
+ private final String redisKey;
+ private final boolean trimQueue;
+ private final int maxQueueSize;
+
+ private AtomicInteger failuresCounter;
+ private Future future;
+
+ TbRedisQueueProcessor(TbContext ctx, TbHttpClient httpClient, boolean trimQueue, int maxQueueSize) {
+ this.ctx = ctx;
+ this.httpClient = httpClient;
+ this.executor = Executors.newSingleThreadExecutor();
+ this.listOperations = ctx.getRedisTemplate().opsForList();
+ this.redisKey = constructRedisKey();
+ this.trimQueue = trimQueue;
+ this.maxQueueSize = maxQueueSize;
+ init();
+ }
+
+ private void init() {
+ failuresCounter = new AtomicInteger(0);
+ future = executor.submit(() -> {
+ while (true) {
+ if (failuresCounter.get() != 0 && failuresCounter.get() % 50 == 0) {
+ sleep("Target HTTP server is down...", 3);
+ }
+ if (listOperations.size(redisKey) > 0) {
+ List list = listOperations.range(redisKey, -10, -1);
+ list.forEach(obj -> {
+ TbMsg msg = TbMsg.fromBytes((byte[]) obj);
+ log.debug("Trying to send the message: {}", msg);
+ listOperations.remove(redisKey, -1, obj);
+ httpClient.processMessage(ctx, msg, this);
+ });
+ } else {
+ sleep("Queue is empty, waiting for tasks!", 1);
+ }
+ }
+ });
+ }
+
+ void destroy() {
+ if (future != null) {
+ future.cancel(true);
+ }
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ }
+
+ void push(TbMsg msg) {
+ listOperations.leftPush(redisKey, TbMsg.toByteArray(msg));
+ if (trimQueue) {
+ listOperations.trim(redisKey, 0, validateMaxQueueSize());
+ }
+ }
+
+ void pushOnFailure(TbMsg msg) {
+ listOperations.rightPush(redisKey, TbMsg.toByteArray(msg));
+ failuresCounter.incrementAndGet();
+ }
+
+ void resetCounter() {
+ failuresCounter.set(0);
+ }
+
+ private String constructRedisKey() {
+ return ctx.getServerAddress() + ctx.getSelfId();
+ }
+
+ private int validateMaxQueueSize() {
+ if (maxQueueSize != 0) {
+ return maxQueueSize;
+ }
+ return MAX_QUEUE_SIZE;
+ }
+
+ private void sleep(String logMessage, int sleepSeconds) {
+ try {
+ log.debug(logMessage);
+ TimeUnit.SECONDS.sleep(sleepSeconds);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException("Thread interrupted!", e);
+ }
+ }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
index e290c18d14..9fa65e775c 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
@@ -15,28 +15,17 @@
*/
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.extern.slf4j.Slf4j;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.ResponseEntity;
-import org.springframework.http.client.Netty4ClientHttpRequestFactory;
-import org.springframework.util.concurrent.ListenableFuture;
-import org.springframework.util.concurrent.ListenableFutureCallback;
-import org.springframework.web.client.AsyncRestTemplate;
-import org.springframework.web.client.HttpClientErrorException;
+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.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.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
@Slf4j
@RuleNode(
@@ -56,107 +45,40 @@ import java.util.concurrent.TimeUnit;
)
public class TbRestApiCallNode implements TbNode {
- private static final String STATUS = "status";
- private static final String STATUS_CODE = "statusCode";
- private static final String STATUS_REASON = "statusReason";
- private static final String ERROR = "error";
- private static final String ERROR_BODY = "error_body";
-
- private TbRestApiCallNodeConfiguration config;
-
- private EventLoopGroup eventLoopGroup;
- private AsyncRestTemplate httpClient;
+ private boolean useRedisQueueForMsgPersistence;
+ private TbHttpClient httpClient;
+ private TbRedisQueueProcessor queueProcessor;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
- try {
- this.config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
- if (this.config.isUseSimpleClientHttpFactory()) {
- httpClient = new AsyncRestTemplate();
- } else {
- this.eventLoopGroup = new NioEventLoopGroup();
- Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup);
- nettyFactory.setSslContext(SslContextBuilder.forClient().build());
- httpClient = new AsyncRestTemplate(nettyFactory);
+ TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class);
+ httpClient = new TbHttpClient(config);
+ useRedisQueueForMsgPersistence = config.isUseRedisQueueForMsgPersistence();
+ if (useRedisQueueForMsgPersistence) {
+ if (ctx.getRedisTemplate() == null) {
+ throw new RuntimeException("Redis cache type must be used!");
}
- } catch (SSLException e) {
- throw new TbNodeException(e);
+ queueProcessor = new TbRedisQueueProcessor(ctx, httpClient, config.isTrimQueue(), config.getMaxQueueSize());
}
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
- String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg.getMetaData());
- HttpHeaders headers = prepareHeaders(msg.getMetaData());
- HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
- HttpEntity entity = new HttpEntity<>(msg.getData(), headers);
-
- ListenableFuture> future = httpClient.exchange(
- endpointUrl, method, entity, String.class);
-
- future.addCallback(new ListenableFutureCallback>() {
- @Override
- public void onFailure(Throwable throwable) {
- TbMsg next = processException(ctx, msg, throwable);
- ctx.tellFailure(next, throwable);
- }
-
- @Override
- public void onSuccess(ResponseEntity responseEntity) {
- if (responseEntity.getStatusCode().is2xxSuccessful()) {
- TbMsg next = processResponse(ctx, msg, responseEntity);
- ctx.tellNext(next, TbRelationTypes.SUCCESS);
- } else {
- TbMsg next = processFailureResponse(ctx, msg, responseEntity);
- ctx.tellNext(next, TbRelationTypes.FAILURE);
- }
- }
- });
+ if (useRedisQueueForMsgPersistence) {
+ queueProcessor.push(msg);
+ } else {
+ httpClient.processMessage(ctx, msg, null);
+ }
}
@Override
public void destroy() {
- if (this.eventLoopGroup != null) {
- this.eventLoopGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS);
+ if (this.httpClient != null) {
+ this.httpClient.destroy();
}
- }
-
- private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) {
- TbMsgMetaData metaData = origMsg.getMetaData();
- metaData.putValue(STATUS, response.getStatusCode().name());
- metaData.putValue(STATUS_CODE, response.getStatusCode().value()+"");
- metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
- response.getHeaders().toSingleValueMap().forEach(metaData::putValue);
- return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, response.getBody());
- }
-
- private TbMsg processFailureResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) {
- TbMsgMetaData metaData = origMsg.getMetaData();
- metaData.putValue(STATUS, response.getStatusCode().name());
- metaData.putValue(STATUS_CODE, response.getStatusCode().value()+"");
- metaData.putValue(STATUS_REASON, response.getStatusCode().getReasonPhrase());
- metaData.putValue(ERROR_BODY, response.getBody());
- return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
- }
-
- private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) {
- TbMsgMetaData metaData = origMsg.getMetaData();
- metaData.putValue(ERROR, e.getClass() + ": " + e.getMessage());
- if (e instanceof HttpClientErrorException) {
- HttpClientErrorException httpClientErrorException = (HttpClientErrorException)e;
- metaData.putValue(STATUS, httpClientErrorException.getStatusText());
- metaData.putValue(STATUS_CODE, httpClientErrorException.getRawStatusCode()+"");
- metaData.putValue(ERROR_BODY, httpClientErrorException.getResponseBodyAsString());
+ if (this.queueProcessor != null) {
+ this.queueProcessor.destroy();
}
- return ctx.transformMsg(origMsg, origMsg.getType(), origMsg.getOriginator(), metaData, origMsg.getData());
- }
-
- private HttpHeaders prepareHeaders(TbMsgMetaData metaData) {
- HttpHeaders headers = new HttpHeaders();
- config.getHeaders().forEach((k,v) -> {
- headers.add(TbNodeUtils.processPattern(k, metaData), TbNodeUtils.processPattern(v, metaData));
- });
- return headers;
}
}
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 6fa3f7151e..0cc3fe7aa2 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
@@ -28,6 +28,11 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration headers;
private boolean useSimpleClientHttpFactory;
+ private int readTimeoutMs;
+ private int maxParallelRequestsCount;
+ private boolean useRedisQueueForMsgPersistence;
+ private boolean trimQueue;
+ private int maxQueueSize;
@Override
public TbRestApiCallNodeConfiguration defaultConfiguration() {
@@ -36,6 +41,10 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration {
- if (m != null) {
- ctx.tellNext(m, SUCCESS);
- } else {
- ctx.tellNext(msg, FAILURE);
- }
- },
- t -> ctx.tellFailure(msg, t));
+ m -> transformSuccess(ctx, msg, m),
+ t -> transformFailure(ctx, msg, t),
+ ctx.getDbCallbackExecutor());
+ }
+
+ protected void transformFailure(TbContext ctx, TbMsg msg, Throwable t) {
+ ctx.tellFailure(msg, t);
+ }
+
+ protected void transformSuccess(TbContext ctx, TbMsg msg, TbMsg m) {
+ if (m != null) {
+ ctx.tellNext(m, SUCCESS);
+ } else {
+ ctx.tellNext(msg, FAILURE);
+ }
}
protected abstract ListenableFuture transform(TbContext ctx, TbMsg msg);
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
index 81c4483166..47e5b3c9c9 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
@@ -21,6 +21,9 @@ import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "script",
@@ -49,7 +52,20 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
@Override
protected ListenableFuture transform(TbContext ctx, TbMsg msg) {
- return ctx.getJsExecutor().executeAsync(() -> jsEngine.executeUpdate(msg));
+ ctx.logJsEvalRequest();
+ return jsEngine.executeUpdateAsync(msg);
+ }
+
+ @Override
+ protected void transformSuccess(TbContext ctx, TbMsg msg, TbMsg m) {
+ ctx.logJsEvalResponse();
+ super.transformSuccess(ctx, msg, m);
+ }
+
+ @Override
+ protected void transformFailure(TbContext ctx, TbMsg msg, Throwable t) {
+ ctx.logJsEvalFailure();
+ super.transformFailure(ctx, msg, t);
}
@Override
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
index 0f252135e2..2ed9e929b1 100644
--- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -1,6 +1,6 @@
-!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={exports:{},id:i,loaded:!1};return e[i].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),i=e[t[0]];return function(e,t,a){i.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(101)},function(e,t){},1,1,1,1,function(e,t){e.exports=" "},function(e,t){e.exports=' attribute.attributes-scope {{scope.name | translate}} '},function(e,t){e.exports=" tb.rulenode.alarm-details-builder {{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
"},function(e,t){e.exports=" tb.rulenode.alarm-details-builder {{ 'tb.rulenode.test-details-function' | translate }}
{{ 'tb.rulenode.use-message-alarm-data' | translate }} tb.rulenode.alarm-type tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
tb.rulenode.alarm-severity {{ severity.name | translate}} tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }} "},function(e,t){e.exports=" relation.direction {{ ('relation.search-direction.' + direction) | translate}} tb.rulenode.relation-type-pattern tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
{{ 'tb.rulenode.create-entity-if-not-exists' | translate }} tb.rulenode.create-entity-if-not-exists-hint
{{ 'tb.rulenode.remove-current-relations' | translate }} tb.rulenode.remove-current-relations-hint
{{ 'tb.rulenode.change-originator-to-related-entity' | translate }} tb.rulenode.change-originator-to-related-entity-hint
tb.rulenode.entity-cache-expiration tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports=" {{ 'tb.rulenode.delete-relation-to-specific-entity' | translate }} tb.rulenode.delete-relation-hint
relation.direction {{ ('relation.search-direction.' + direction) | translate}} tb.rulenode.relation-type-pattern tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
tb.rulenode.entity-cache-expiration tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports=" tb.rulenode.message-count tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.originator' | translate }}
tb.rulenode.generate {{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports=' '},function(e,t){e.exports=' '},function(e,t){e.exports=" tb.rulenode.to-string {{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports=' tb.rulenode.topic-pattern tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.client-id {{ \'tb.rulenode.clean-session\' | translate }} {{ \'tb.rulenode.enable-ssl\' | translate }} {{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
tb.rulenode.credentials-type {{credentialsValue.name | translate}} tb.rulenode.credentials-type-required
tb.rulenode.no-file
{{configuration.credentials.caCertFileName}}
tb.rulenode.no-file
{{configuration.credentials.certFileName}}
tb.rulenode.no-file
{{configuration.credentials.privateKeyFileName}}
tb.rulenode.private-key-password '},function(e,t){e.exports=" ";
-},function(e,t){e.exports=' '},function(e,t){e.exports=" "},function(e,t){e.exports=' '},function(e,t){e.exports=' tb.rulenode.endpoint-url-pattern tb.rulenode.endpoint-url-pattern-required
tb.rulenode.endpoint-url-pattern-hint
tb.rulenode.request-method {{ type }} {{ \'tb.rulenode.use-simple-client-http-factory\' | translate }} tb.rulenode.headers tb.rulenode.headers-hint
'},function(e,t){e.exports=" "},function(e,t){e.exports=" "},function(e,t){e.exports=' '},function(e,t){e.exports=' {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} '},function(e,t){e.exports=" "},function(e,t){e.exports=' '},function(e,t){e.exports=" "},function(e,t){e.exports=" "},function(e,t){e.exports=' relation.direction {{ (\'relation.search-direction.\' + direction) | translate}} tb.rulenode.max-relation-level
relation.relation-type
device.device-types
'},function(e,t){e.exports=" tb.rulenode.attr-mapping {{ 'tb.rulenode.latest-telemetry' | translate }} "},function(e,t){e.exports=' tb.rulenode.device-relations-query {{ \'tb.rulenode.tell-failure-if-absent\' | translate }} tb.rulenode.tell-failure-if-absent-hint
tb.rulenode.client-attributes tb.rulenode.shared-attributes tb.rulenode.server-attributes tb.rulenode.latest-timeseries '},function(e,t){e.exports=' tb.rulenode.entity-details {{\'tb.rulenode.entity-details-\'+item.toLowerCase() | translate}} tb.rulenode.no-entity-details-matching {{\'tb.rulenode.entity-details-\'+$chip.toLowerCase() | translate}} tb.rulenode.entity-details-list-empty
{{ \'tb.rulenode.add-to-metadata\' | translate }} tb.rulenode.add-to-metadata-hint
'},function(e,t){e.exports=' tb.rulenode.latest-timeseries tb.rulenode.fetch-mode {{ type }} tb.rulenode.fetch-mode-hint
tb.rulenode.order-by {{ type }} tb.rulenode.order-by-hint
{{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }} tb.rulenode.use-metadata-interval-patterns-hint
tb.rulenode.start-interval tb.rulenode.start-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
tb.rulenode.start-interval-time-unit {{timeUnit.name | translate}} tb.rulenode.end-interval tb.rulenode.end-interval-value-required
tb.rulenode.time-value-range
tb.rulenode.time-value-range
tb.rulenode.end-interval-time-unit {{timeUnit.name | translate}} tb.rulenode.start-interval-pattern tb.rulenode.start-interval-pattern-required
tb.rulenode.start-interval-pattern-hint
tb.rulenode.end-interval-pattern tb.rulenode.end-interval-pattern-required
tb.rulenode.end-interval-pattern-hint
'},function(e,t){e.exports=' {{ \'tb.rulenode.tell-failure-if-absent\' | translate }} tb.rulenode.tell-failure-if-absent-hint
tb.rulenode.client-attributes tb.rulenode.shared-attributes tb.rulenode.server-attributes tb.rulenode.latest-timeseries '},function(e,t){e.exports=' tb.rulenode.fields-mapping ';
-},function(e,t){e.exports=" tb.rulenode.relations-query tb.rulenode.attr-mapping {{ 'tb.rulenode.latest-telemetry' | translate }} "},31,function(e,t){e.exports=' tb.rulenode.data-keys tb.rulenode.separator-hint
tb.rulenode.metadata-keys tb.rulenode.separator-hint
{{ \'tb.rulenode.check-all-keys\' | translate }} tb.rulenode.check-all-keys-hint
'},function(e,t){e.exports=" {{ 'tb.rulenode.check-relation-to-specific-entity' | translate }} tb.rulenode.check-relation-hint
relation.direction {{ ('relation.search-direction.' + direction) | translate}}
"},function(e,t){e.exports=' '},function(e,t){e.exports=' tb.rulenode.message-types-filter {{item}} tb.rulenode.no-message-types-found
{{$chip.name}} tb.rulenode.message-types-required
'},function(e,t){e.exports=' tb.rulenode.originator-types-filter '},function(e,t){e.exports=" tb.rulenode.filter {{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports=" tb.rulenode.switch {{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports=' {{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports=" "},function(e,t){e.exports=' tb.rulenode.originator-source {{ source.name | translate}} tb.rulenode.relations-query '},function(e,t){e.exports=" tb.rulenode.transform {{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports=" tb.rulenode.from-template tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.cc-template tb.rulenode.mail-address-list-template-hint
tb.rulenode.bcc-template tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(6),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(7),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(8),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(9),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(10),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(11),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript);i.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var r=n(12),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(13),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(74),r=i(a),o=n(52),l=i(o),s=n(57),d=i(s),u=n(54),c=i(u),m=n(53),g=i(m),p=n(61),f=i(p),b=n(68),v=i(b),y=n(69),h=i(y),q=n(67),$=i(q),x=n(60),k=i(x),T=n(72),C=i(T),w=n(73),M=i(w),N=n(66),_=i(N),S=n(62),E=i(S),F=n(71),P=i(F),A=n(64),V=i(A),I=n(63),j=i(I),O=n(51),D=i(O),R=n(75),K=i(R),L=n(56),U=i(L),z=n(55),B=i(z),H=n(70),G=i(H),Y=n(58),Q=i(Y),J=n(65),W=i(J);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",r.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",d.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",h.default).directive("tbActionNodeRestApiCallConfig",$.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",_.default).directive("tbActionNodeMqttConfig",E.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",V.default).directive("tbActionNodeMsgCountConfig",j.default).directive("tbActionNodeAssignToCustomerConfig",D.default).directive("tbActionNodeUnAssignToCustomerConfig",K.default).directive("tbActionNodeDeleteRelationConfig",U.default).directive("tbActionNodeCreateRelationConfig",B.default).directive("tbActionNodeCustomTableConfig",G.default).directive("tbActionNodeGpsGeofencingConfig",Q.default).directive("tbActionNodePubSubConfig",W.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(14),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(15),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$mdExpansionPanel=t,i.ruleNodeTypes=n,i.credentialsTypeChanged=function(){var e=i.configuration.credentials.type;i.configuration.credentials={},i.configuration.credentials.type=e,i.updateValidity()},i.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){i.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(i.configuration.credentials.caCertFileName=e.name,i.configuration.credentials.caCert=a),"privateKey"==t&&(i.configuration.credentials.privateKeyFileName=e.name,i.configuration.credentials.privateKey=a),"Cert"==t&&(i.configuration.credentials.certFileName=e.name,i.configuration.credentials.cert=a)),i.updateValidity()}})},n.readAsText(e.file)},i.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(i.configuration.credentials.caCertFileName=null,i.configuration.credentials.caCert=null),"privateKey"==e&&(i.configuration.credentials.privateKeyFileName=null,i.configuration.credentials.privateKey=null),"Cert"==e&&(i.configuration.credentials.certFileName=null,i.configuration.credentials.cert=null),i.updateValidity()},i.updateValidity=function(){var e=!0,t=i.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:i}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var r=n(16),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(17),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(18),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.serviceAccountFileAdded=function(e){var t=new FileReader;t.onload=function(t){n.$apply(function(){if(t.target.result){r.$setDirty();var i=t.target.result;i&&i.length>0&&(n.configuration.serviceAccountKeyFileName=e.name,n.configuration.serviceAccountKey=i),n.updateValidity()}})},t.readAsText(e.file)},n.clearServiceAccountFile=function(){r.$setDirty(),n.configuration.serviceAccountKeyFileName=null,n.configuration.serviceAccountKey=null,n.updateValidity()},n.updateValidity=function(){var e=!0,t=n.configuration;t.serviceAccountKeyFileName&&t.serviceAccountKey||(e=!1),r.$setValidity("SAKey",e)},n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(19),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(20),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(21),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(22),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(23),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(24),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"
-},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(25),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(26),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(27),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(28),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(29),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(30),o=i(r)},function(e,t){"use strict";function n(e){var t=function(t,n,i,a){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(31),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(32),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),n.entityDetailsList=[];for(var s in t.entityDetails){var d=s;n.entityDetailsList.push(d)}r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(33),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s);var d=186;i.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,d],i.ruleNodeTypes=n,i.aggPeriodTimeUnits={},i.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,i.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,i.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,i.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,i.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{},link:i}}a.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(34),o=i(r);n(3)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(83),r=i(a),o=n(84),l=i(o),s=n(79),d=i(s),u=n(85),c=i(u),m=n(78),g=i(m),p=n(86),f=i(p),b=n(81),v=i(b),y=n(80),h=i(y);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",r.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",d.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).directive("tbEnrichmentNodeEntityDetailsConfig",h.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(35),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(36),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(37),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(38),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(39),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(40),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(41),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(93),r=i(a),o=n(91),l=i(o),s=n(94),d=i(s),u=n(88),c=i(u),m=n(92),g=i(m),p=n(87),f=i(p),b=n(89),v=i(b);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",r.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",d.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).directive("tbFilterNodeCheckMessageConfig",f.default).directive("tbFilterNodeGpsGeofencingConfig",v.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),d()}function d(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var u=o.default;n.html(u),t.ngModelCtrl=a,t.removeKeyVal=r,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),d()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(46),o=i(r);n(5)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(47),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(48),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(97),r=i(a),o=n(99),l=i(o),s=n(100),d=i(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",r.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",d.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(49),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(50),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(104),r=i(a),o=n(90),l=i(o),s=n(82),d=i(s),u=n(98),c=i(u),m=n(59),g=i(m),p=n(77),f=i(p),b=n(96),v=i(b),y=n(76),h=i(y),q=n(95),$=i(q),x=n(103),k=i(x);t.default=angular.module("thingsboard.ruleChain.config",[r.default,l.default,d.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",h.default).directive("tbKvMapConfig",$.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={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.","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","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-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","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",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",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","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","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","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","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".'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};e.translations("en_US",t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){(0,o.default)(e)}a.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(102),o=i(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"},ALARM_ORIGINATOR:{name:"tb.rulenode.originator-alarm-originator",value:"ALARM_ORIGINATOR"}},fetchModeType:["FIRST","LAST","ALL"],samplingOrder:["ASC","DESC"],httpRequestType:["GET","POST","PUT","DELETE"],entityDetails:{TITLE:{name:"tb.rulenode.entity-details-title",value:"TITLE"},COUNTRY:{name:"tb.rulenode.entity-details-country",value:"COUNTRY"},STATE:{name:"tb.rulenode.entity-details-state",value:"STATE"},ZIP:{name:"tb.rulenode.entity-details-zip",value:"ZIP"},ADDRESS:{name:"tb.rulenode.entity-details-address",value:"ADDRESS"},ADDRESS2:{name:"tb.rulenode.entity-details-address2",value:"ADDRESS2"},PHONE:{name:"tb.rulenode.entity-details-phone",value:"PHONE"},EMAIL:{name:"tb.rulenode.entity-details-email",value:"EMAIL"},ADDITIONAL_INFO:{name:"tb.rulenode.entity-details-additional_info",value:"ADDITIONAL_INFO"}},sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},perimeterType:{CIRCLE:{name:"tb.rulenode.perimeter-circle",value:"CIRCLE"},POLYGON:{name:"tb.rulenode.perimeter-polygon",value:"POLYGON"}},timeUnit:{MILLISECONDS:{value:"MILLISECONDS",name:"tb.rulenode.time-unit-milliseconds"},SECONDS:{value:"SECONDS",name:"tb.rulenode.time-unit-seconds"},MINUTES:{value:"MINUTES",name:"tb.rulenode.time-unit-minutes"},HOURS:{value:"HOURS",name:"tb.rulenode.time-unit-hours"},DAYS:{value:"DAYS",name:"tb.rulenode.time-unit-days"}},rangeUnit:{METER:{value:"METER",name:"tb.rulenode.range-unit-meter"},KILOMETER:{value:"KILOMETER",name:"tb.rulenode.range-unit-kilometer"},FOOT:{value:"FOOT",name:"tb.rulenode.range-unit-foot"},MILE:{value:"MILE",name:"tb.rulenode.range-unit-mile"},NAUTICAL_MILE:{value:"NAUTICAL_MILE",name:"tb.rulenode.range-unit-nautical-mile"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}]));
+!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={exports:{},id:i,loaded:!1};return e[i].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),i=e[t[0]];return function(e,t,a){i.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(101)},function(e,t){},1,1,1,1,function(e,t){e.exports=" "},function(e,t){e.exports=' attribute.attributes-scope {{scope.name | translate}} '},function(e,t){e.exports=" tb.rulenode.alarm-details-builder {{ 'tb.rulenode.test-details-function' | translate }}
tb.rulenode.alarm-type tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
"},function(e,t){e.exports=" tb.rulenode.alarm-details-builder {{ 'tb.rulenode.test-details-function' | translate }}
{{ 'tb.rulenode.use-message-alarm-data' | translate }} tb.rulenode.alarm-type tb.rulenode.alarm-type-required
tb.rulenode.entity-type-pattern-hint
tb.rulenode.alarm-severity {{ severity.name | translate}} tb.rulenode.alarm-severity-required
{{ 'tb.rulenode.propagate' | translate }} "},function(e,t){e.exports=" relation.direction {{ ('relation.search-direction.' + direction) | translate}} tb.rulenode.relation-type-pattern tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
{{ 'tb.rulenode.create-entity-if-not-exists' | translate }} tb.rulenode.create-entity-if-not-exists-hint
{{ 'tb.rulenode.remove-current-relations' | translate }} tb.rulenode.remove-current-relations-hint
{{ 'tb.rulenode.change-originator-to-related-entity' | translate }} tb.rulenode.change-originator-to-related-entity-hint
tb.rulenode.entity-cache-expiration tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports=" {{ 'tb.rulenode.delete-relation-to-specific-entity' | translate }} tb.rulenode.delete-relation-hint
relation.direction {{ ('relation.search-direction.' + direction) | translate}} tb.rulenode.relation-type-pattern tb.rulenode.relation-type-pattern-required
tb.rulenode.relation-type-pattern-hint
tb.rulenode.entity-cache-expiration tb.rulenode.entity-cache-expiration-required
tb.rulenode.entity-cache-expiration-range
tb.rulenode.entity-cache-expiration-hint
"},function(e,t){e.exports=" tb.rulenode.message-count tb.rulenode.message-count-required
tb.rulenode.min-message-count-message
tb.rulenode.period-seconds tb.rulenode.period-seconds-required
tb.rulenode.min-period-seconds-message
{{ 'tb.rulenode.originator' | translate }}
tb.rulenode.generate {{ 'tb.rulenode.test-generator-function' | translate }}
"},function(e,t){e.exports=' '},function(e,t){e.exports=' '},function(e,t){e.exports=" tb.rulenode.to-string {{ 'tb.rulenode.test-to-string-function' | translate }}
"},function(e,t){e.exports=' tb.rulenode.topic-pattern tb.rulenode.topic-pattern-required
tb.rulenode.mqtt-topic-pattern-hint
tb.rulenode.client-id {{ \'tb.rulenode.clean-session\' | translate }} {{ \'tb.rulenode.enable-ssl\' | translate }} {{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
{{ \'tb.rulenode.credentials\' | translate }}
{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}
tb.rulenode.credentials-type {{credentialsValue.name | translate}} tb.rulenode.credentials-type-required
tb.rulenode.no-file
{{configuration.credentials.caCertFileName}}
tb.rulenode.no-file
{{configuration.credentials.certFileName}}
tb.rulenode.no-file
{{configuration.credentials.privateKeyFileName}}
tb.rulenode.private-key-password '},function(e,t){e.exports=" ";
+},function(e,t){e.exports=' '},function(e,t){e.exports=" "},function(e,t){e.exports=' '},function(e,t){e.exports=' '},function(e,t){e.exports=" "},function(e,t){e.exports=" "},function(e,t){e.exports=' '},function(e,t){e.exports=' {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} '},function(e,t){e.exports=" "},function(e,t){e.exports=' '},function(e,t){e.exports=" "},function(e,t){e.exports=" "},function(e,t){e.exports=' relation.direction {{ (\'relation.search-direction.\' + direction) | translate}} tb.rulenode.max-relation-level
relation.relation-type
device.device-types
'},function(e,t){e.exports=" tb.rulenode.attr-mapping {{ 'tb.rulenode.latest-telemetry' | translate }} "},function(e,t){e.exports=' tb.rulenode.device-relations-query {{ \'tb.rulenode.tell-failure-if-absent\' | translate }} tb.rulenode.tell-failure-if-absent-hint
tb.rulenode.client-attributes tb.rulenode.shared-attributes tb.rulenode.server-attributes tb.rulenode.latest-timeseries {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }} tb.rulenode.get-latest-value-with-ts-hint
'},function(e,t){e.exports=' tb.rulenode.entity-details {{\'tb.rulenode.entity-details-\'+item.toLowerCase() | translate}} tb.rulenode.no-entity-details-matching {{\'tb.rulenode.entity-details-\'+$chip.toLowerCase() | translate}} tb.rulenode.entity-details-list-empty
{{ \'tb.rulenode.add-to-metadata\' | translate }} tb.rulenode.add-to-metadata-hint
'},function(e,t){e.exports=' ';
+},function(e,t){e.exports=' {{ \'tb.rulenode.tell-failure-if-absent\' | translate }} tb.rulenode.tell-failure-if-absent-hint
tb.rulenode.client-attributes tb.rulenode.shared-attributes tb.rulenode.server-attributes tb.rulenode.latest-timeseries {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }} tb.rulenode.get-latest-value-with-ts-hint
'},function(e,t){e.exports=' tb.rulenode.fields-mapping '},function(e,t){e.exports=" tb.rulenode.relations-query tb.rulenode.attr-mapping {{ 'tb.rulenode.latest-telemetry' | translate }} "},31,function(e,t){e.exports=' tb.rulenode.data-keys tb.rulenode.separator-hint
tb.rulenode.metadata-keys tb.rulenode.separator-hint
{{ \'tb.rulenode.check-all-keys\' | translate }} tb.rulenode.check-all-keys-hint
'},function(e,t){e.exports=" {{ 'tb.rulenode.check-relation-to-specific-entity' | translate }} tb.rulenode.check-relation-hint
relation.direction {{ ('relation.search-direction.' + direction) | translate}}
"},function(e,t){e.exports=' '},function(e,t){e.exports=' tb.rulenode.message-types-filter {{item}} tb.rulenode.no-message-types-found
{{$chip.name}} tb.rulenode.message-types-required
'},function(e,t){e.exports=' tb.rulenode.originator-types-filter '},function(e,t){e.exports=" tb.rulenode.filter {{ 'tb.rulenode.test-filter-function' | translate }}
"},function(e,t){e.exports=" tb.rulenode.switch {{ 'tb.rulenode.test-switch-function' | translate }}
"},function(e,t){e.exports=' {{ \'tb.key-val.add-entry\' | translate }} add {{ \'action.add\' | translate }}
'},function(e,t){e.exports=" "},function(e,t){e.exports=' tb.rulenode.originator-source {{ source.name | translate}} tb.rulenode.relations-query '},function(e,t){e.exports=" tb.rulenode.transform {{ 'tb.rulenode.test-transformer-function' | translate }}
"},function(e,t){e.exports=" tb.rulenode.from-template tb.rulenode.from-template-required
tb.rulenode.from-template-hint
tb.rulenode.to-template tb.rulenode.to-template-required
tb.rulenode.mail-address-list-template-hint
tb.rulenode.cc-template tb.rulenode.mail-address-list-template-hint
tb.rulenode.bcc-template tb.rulenode.mail-address-list-template-hint
tb.rulenode.subject-template tb.rulenode.subject-template-required
tb.rulenode.subject-template-hint
tb.rulenode.body-template tb.rulenode.body-template-required
tb.rulenode.body-template-hint
"},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(6),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(7),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(8),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs);i.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(9),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(10),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(11),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,i){var a=function(a,r,l,s){var d=o.default;r.html(d),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript);i.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var r=n(12),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(13),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(74),r=i(a),o=n(52),l=i(o),s=n(57),d=i(s),u=n(54),c=i(u),m=n(53),g=i(m),p=n(61),f=i(p),b=n(68),v=i(b),y=n(69),h=i(y),q=n(67),x=i(q),k=n(60),$=i(k),T=n(72),C=i(T),w=n(73),M=i(w),N=n(66),S=i(N),_=n(62),E=i(_),F=n(71),P=i(F),A=n(64),V=i(A),I=n(63),j=i(I),O=n(51),D=i(O),R=n(75),K=i(R),L=n(56),U=i(L),z=n(55),H=i(z),B=n(70),G=i(B),Y=n(58),Q=i(Y),W=n(65),J=i(W);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",r.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",d.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",h.default).directive("tbActionNodeRestApiCallConfig",x.default).directive("tbActionNodeKafkaConfig",$.default).directive("tbActionNodeSnsConfig",C.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",S.default).directive("tbActionNodeMqttConfig",E.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",V.default).directive("tbActionNodeMsgCountConfig",j.default).directive("tbActionNodeAssignToCustomerConfig",D.default).directive("tbActionNodeUnAssignToCustomerConfig",K.default).directive("tbActionNodeDeleteRelationConfig",U.default).directive("tbActionNodeCreateRelationConfig",H.default).directive("tbActionNodeCustomTableConfig",G.default).directive("tbActionNodeGpsGeofencingConfig",Q.default).directive("tbActionNodePubSubConfig",J.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(14),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(15),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$mdExpansionPanel=t,i.ruleNodeTypes=n,i.credentialsTypeChanged=function(){var e=i.configuration.credentials.type;i.configuration.credentials={},i.configuration.credentials.type=e,i.updateValidity()},i.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){i.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(i.configuration.credentials.caCertFileName=e.name,i.configuration.credentials.caCert=a),"privateKey"==t&&(i.configuration.credentials.privateKeyFileName=e.name,i.configuration.credentials.privateKey=a),"Cert"==t&&(i.configuration.credentials.certFileName=e.name,i.configuration.credentials.cert=a)),i.updateValidity()}})},n.readAsText(e.file)},i.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(i.configuration.credentials.caCertFileName=null,i.configuration.credentials.caCert=null),"privateKey"==e&&(i.configuration.credentials.privateKeyFileName=null,i.configuration.credentials.privateKey=null),"Cert"==e&&(i.configuration.credentials.certFileName=null,i.configuration.credentials.cert=null),i.updateValidity()},i.updateValidity=function(){var e=!0,t=i.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:i}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var r=n(16),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(17),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(18),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.serviceAccountFileAdded=function(e){var t=new FileReader;t.onload=function(t){n.$apply(function(){if(t.target.result){r.$setDirty();var i=t.target.result;i&&i.length>0&&(n.configuration.serviceAccountKeyFileName=e.name,n.configuration.serviceAccountKey=i),n.updateValidity()}})},t.readAsText(e.file)},n.clearServiceAccountFile=function(){r.$setDirty(),n.configuration.serviceAccountKeyFileName=null,n.configuration.serviceAccountKey=null,n.updateValidity()},n.updateValidity=function(){var e=!0,t=n.configuration;t.serviceAccountKeyFileName&&t.serviceAccountKey||(e=!1),r.$setValidity("SAKey",e)},n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(19),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],
+Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(20),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(21),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(22),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(23),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(24),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(25),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(26),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(27),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(28),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(29),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(30),o=i(r)},function(e,t){"use strict";function n(e){var t=function(t,n,i,a){n.html("
"),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}n.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(31),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(32),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),n.entityDetailsList=[];for(var s in t.entityDetails){var d=s;n.entityDetailsList.push(d)}r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(33),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s);var d=186;i.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,d],i.ruleNodeTypes=n,i.aggPeriodTimeUnits={},i.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,i.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,i.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,i.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,i.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{},link:i}}a.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(34),o=i(r);n(3)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(83),r=i(a),o=n(84),l=i(o),s=n(79),d=i(s),u=n(85),c=i(u),m=n(78),g=i(m),p=n(86),f=i(p),b=n(81),v=i(b),y=n(80),h=i(y);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",r.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",d.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).directive("tbEnrichmentNodeEntityDetailsConfig",h.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(35),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(36),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(37),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(38),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(39),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(40),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(41),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(93),r=i(a),o=n(91),l=i(o),s=n(94),d=i(s),u=n(88),c=i(u),m=n(92),g=i(m),p=n(87),f=i(p),b=n(89),v=i(b);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",r.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",d.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).directive("tbFilterNodeCheckMessageConfig",f.default).directive("tbFilterNodeGpsGeofencingConfig",v.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){function s(){if(l.$viewValue){for(var e=[],t=0;t-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),d()}function d(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var u=o.default;n.html(u),t.ngModelCtrl=a,t.removeKeyVal=r,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),d()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(46),o=i(r);n(5)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||r.$setViewValue(n.query)}),r.$render=function(){n.query=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(47),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,i,a,r){var l=o.default;i.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||r.$setViewValue(n.configuration)}),r.$render=function(){n.configuration=r.$viewValue},e(i.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(48),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(97),r=i(a),o=n(99),l=i(o),s=n(100),d=i(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",r.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",d.default).name},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var i=function(i,a,r,l){var s=o.default;a.html(s),i.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(i.configuration)}),l.$render=function(){i.configuration=l.$viewValue},i.testScript=function(e){var a=angular.copy(i.configuration.jsScript);n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i.ruleNodeId).then(function(e){i.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(i)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:i}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(49),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,i,a){var r=o.default;n.html(r),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(50),o=i(r)},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(104),r=i(a),o=n(90),l=i(o),s=n(82),d=i(s),u=n(98),c=i(u),m=n(59),g=i(m),p=n(77),f=i(p),b=n(96),v=i(b),y=n(76),h=i(y),q=n(95),x=i(q),k=n(103),$=i(k);t.default=angular.module("thingsboard.ruleChain.config",[r.default,l.default,d.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",h.default).directive("tbKvMapConfig",x.default).config($.default).name},function(e,t){"use strict";function n(e){var t={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","shared-attributes":"Shared attributes","server-attributes":"Server attributes","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-hint":"Relation type pattern, use ${metaKeyName} to substitute variables from metadata","relation-type-pattern-required":"Relation type pattern is required","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",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","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","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","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","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"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};e.translations("en_US",t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function a(e){(0,o.default)(e)}a.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var r=n(102),o=i(r)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"},ALARM_ORIGINATOR:{name:"tb.rulenode.originator-alarm-originator",value:"ALARM_ORIGINATOR"}},fetchModeType:["FIRST","LAST","ALL"],samplingOrder:["ASC","DESC"],httpRequestType:["GET","POST","PUT","DELETE"],entityDetails:{TITLE:{name:"tb.rulenode.entity-details-title",value:"TITLE"},COUNTRY:{name:"tb.rulenode.entity-details-country",value:"COUNTRY"},STATE:{name:"tb.rulenode.entity-details-state",value:"STATE"},ZIP:{name:"tb.rulenode.entity-details-zip",value:"ZIP"},ADDRESS:{name:"tb.rulenode.entity-details-address",value:"ADDRESS"},ADDRESS2:{name:"tb.rulenode.entity-details-address2",value:"ADDRESS2"},PHONE:{name:"tb.rulenode.entity-details-phone",value:"PHONE"},EMAIL:{name:"tb.rulenode.entity-details-email",value:"EMAIL"},ADDITIONAL_INFO:{name:"tb.rulenode.entity-details-additional_info",value:"ADDITIONAL_INFO"}},sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},perimeterType:{CIRCLE:{name:"tb.rulenode.perimeter-circle",value:"CIRCLE"},POLYGON:{name:"tb.rulenode.perimeter-polygon",value:"POLYGON"}},timeUnit:{MILLISECONDS:{value:"MILLISECONDS",name:"tb.rulenode.time-unit-milliseconds"},SECONDS:{value:"SECONDS",name:"tb.rulenode.time-unit-seconds"},MINUTES:{value:"MINUTES",name:"tb.rulenode.time-unit-minutes"},HOURS:{value:"HOURS",name:"tb.rulenode.time-unit-hours"},DAYS:{value:"DAYS",name:"tb.rulenode.time-unit-days"}},rangeUnit:{METER:{value:"METER",name:"tb.rulenode.range-unit-meter"},KILOMETER:{value:"KILOMETER",name:"tb.rulenode.range-unit-kilometer"},FOOT:{value:"FOOT",name:"tb.rulenode.range-unit-foot"},MILE:{value:"MILE",name:"tb.rulenode.range-unit-mile"},NAUTICAL_MILE:{value:"NAUTICAL_MILE",name:"tb.rulenode.range-unit-nautical-mile"}},mqttCredentialTypes:{anonymous:{value:"anonymous",name:"tb.rulenode.credentials-anonymous"},basic:{value:"basic",name:"tb.rulenode.credentials-basic"},"cert.PEM":{value:"cert.PEM",name:"tb.rulenode.credentials-pem"}}}).name}]));
//# sourceMappingURL=rulenode-core-config.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 32318edcb8..fb07951368 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
@@ -28,6 +28,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.id.DeviceId;
@@ -156,6 +157,7 @@ public class TbAlarmNodeTest {
verify(ctx, times(1)).getJsExecutor();
verify(ctx).getAlarmService();
verify(ctx, times(3)).getDbCallbackExecutor();
+ verify(ctx).logJsEvalRequest();
verify(ctx).getTenantId();
verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
@@ -270,6 +272,7 @@ public class TbAlarmNodeTest {
// when(detailsJs.executeJson(msg)).thenReturn(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())).thenReturn(Futures.immediateFuture(true));
+ when(alarmService.findAlarmByIdAsync(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()))).thenReturn(Futures.immediateFuture(activeAlarm));
// doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
node.onMsg(ctx, msg);
@@ -375,4 +378,4 @@ public class TbAlarmNodeTest {
assertEquals(message, value.getMessage());
}
-}
\ No newline at end of file
+}
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 f1aace48bb..d30a2f8626 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
@@ -19,6 +19,7 @@ import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -26,6 +27,7 @@ import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -58,10 +60,10 @@ public class TbJsFilterNodeTest {
initWithScript();
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}", ruleChainId, ruleNodeId, 0L);
mockJsExecutor();
- when(scriptEngine.executeFilter(msg)).thenReturn(false);
+ when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(false));
node.onMsg(ctx, msg);
- verify(ctx).getJsExecutor();
+ verify(ctx).getDbCallbackExecutor();
verify(ctx).tellNext(msg, "False");
}
@@ -71,7 +73,7 @@ public class TbJsFilterNodeTest {
TbMsgMetaData metaData = new TbMsgMetaData();
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L);
mockJsExecutor();
- when(scriptEngine.executeFilter(msg)).thenThrow(new ScriptException("error"));
+ when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFailedFuture(new ScriptException("error")));
node.onMsg(ctx, msg);
@@ -84,10 +86,10 @@ public class TbJsFilterNodeTest {
TbMsgMetaData metaData = new TbMsgMetaData();
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}", ruleChainId, ruleNodeId, 0L);
mockJsExecutor();
- when(scriptEngine.executeFilter(msg)).thenReturn(true);
+ when(scriptEngine.executeFilterAsync(msg)).thenReturn(Futures.immediateFuture(true));
node.onMsg(ctx, msg);
- verify(ctx).getJsExecutor();
+ verify(ctx).getDbCallbackExecutor();
verify(ctx).tellNext(msg, "True");
}
@@ -123,4 +125,4 @@ public class TbJsFilterNodeTest {
assertEquals(expectedClass, value.getClass());
assertEquals(message, value.getMessage());
}
-}
\ No newline at end of file
+}
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 82ecbb4ba8..eca1a4a684 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
@@ -27,6 +27,7 @@ import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -105,4 +106,4 @@ public class TbJsSwitchNodeTest {
assertEquals(expectedClass, value.getClass());
assertEquals(message, value.getMessage());
}
-}
\ No newline at end of file
+}
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 feb561bb97..d31d184127 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
@@ -25,7 +25,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
-import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
@@ -166,4 +166,4 @@ public class TbChangeOriginatorNodeTest {
node = new TbChangeOriginatorNode();
node.init(null, nodeConfiguration);
}
-}
\ No newline at end of file
+}
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 7f577255da..f6464caf15 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
@@ -26,6 +26,7 @@ import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
+import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -64,10 +65,10 @@ public class TbTransformMsgNodeTest {
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}", ruleChainId, ruleNodeId, 0L);
mockJsExecutor();
- when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg);
+ when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFuture(transformedMsg));
node.onMsg(ctx, msg);
- verify(ctx).getJsExecutor();
+ verify(ctx).getDbCallbackExecutor();
ArgumentCaptor captor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx).tellNext(captor.capture(), eq(SUCCESS));
TbMsg actualMsg = captor.getValue();
@@ -85,7 +86,7 @@ public class TbTransformMsgNodeTest {
RuleNodeId ruleNodeId = new RuleNodeId(UUIDs.timeBased());
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
mockJsExecutor();
- when(scriptEngine.executeUpdate(msg)).thenThrow(new IllegalStateException("error"));
+ when(scriptEngine.executeUpdateAsync(msg)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("error")));
node.onMsg(ctx, msg);
verifyError(msg, "error", IllegalStateException.class);
@@ -123,4 +124,4 @@ public class TbTransformMsgNodeTest {
assertEquals(expectedClass, value.getClass());
assertEquals(message, value.getMessage());
}
-}
\ No newline at end of file
+}
diff --git a/tools/pom.xml b/tools/pom.xml
index 88f8ccdacb..cc215257dd 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 2.4.2-SNAPSHOT
+ 2.4.2
thingsboard
tools
@@ -51,6 +51,39 @@
com.google.guava
guava
+
+ org.apache.cassandra
+ cassandra-all
+ 3.11.4
+
+
+ com.datastax.cassandra
+ cassandra-driver-core
+
+
+ commons-io
+ commons-io
+
+
+
+
+
+ maven-assembly-plugin
+
+
+
+ org.thingsboard.client.tools.migrator.MigratorTool
+
+
+
+ jar-with-dependencies
+
+
+
+
+
+
+
diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
index 44f345dc96..97ce718cc8 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
@@ -16,7 +16,11 @@
package org.thingsboard.client.tools;
import com.fasterxml.jackson.databind.JsonNode;
-import lombok.RequiredArgsConstructor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -24,43 +28,129 @@ import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
+import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.ClaimRequest;
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.Device;
+import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.EntityView;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.UpdateMessage;
+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.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.AssetSearchQuery;
+import org.thingsboard.server.common.data.audit.AuditLog;
+import org.thingsboard.server.common.data.device.DeviceSearchQuery;
+import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
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.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.common.data.security.model.SecuritySettings;
+import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.io.IOException;
+import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
+import static org.springframework.util.StringUtils.isEmpty;
+
/**
* @author Andrew Shvayka
*/
-@RequiredArgsConstructor
public class RestClient implements ClientHttpRequestInterceptor {
private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
- private final RestTemplate restTemplate = new RestTemplate();
+ protected final RestTemplate restTemplate;
+ protected final String baseURL;
private String token;
- private final String baseURL;
+ private String refreshToken;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+
+ protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken=";
+
+ public RestClient(String baseURL) {
+ this.restTemplate = new RestTemplate();
+ this.baseURL = baseURL;
+ }
+
+ public RestClient(RestTemplate restTemplate, String baseURL) {
+ this.restTemplate = restTemplate;
+ this.baseURL = baseURL;
+ }
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
+ HttpRequest wrapper = new HttpRequestWrapper(request);
+ wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
+ ClientHttpResponse response = execution.execute(wrapper, bytes);
+ if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
+ synchronized (this) {
+ restTemplate.getInterceptors().remove(this);
+ refreshToken();
+ wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
+ return execution.execute(wrapper, bytes);
+ }
+ }
+ return response;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public String getRefreshToken() {
+ return refreshToken;
+ }
+
+ public void refreshToken() {
+ Map refreshTokenRequest = new HashMap<>();
+ refreshTokenRequest.put("refreshToken", refreshToken);
+ ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class);
+ setTokenInfo(tokenInfo.getBody());
+ }
public void login(String username, String password) {
Map loginRequest = new HashMap<>();
loginRequest.put("username", username);
loginRequest.put("password", password);
ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class);
- this.token = tokenInfo.getBody().get("token").asText();
- restTemplate.setInterceptors(Collections.singletonList(this));
+ setTokenInfo(tokenInfo.getBody());
+ }
+
+ private void setTokenInfo(JsonNode tokenInfo) {
+ this.token = tokenInfo.get("token").asText();
+ this.refreshToken = tokenInfo.get("refreshToken").asText();
+ restTemplate.getInterceptors().add(this);
}
public Optional findDevice(String name) {
@@ -149,10 +239,6 @@ public class RestClient implements ClientHttpRequestInterceptor {
return saveDeviceCredentials(deviceCredentials);
}
- public DeviceCredentials saveDeviceCredentials(DeviceCredentials deviceCredentials) {
- return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody();
- }
-
public Device createDevice(Device device) {
return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
}
@@ -190,7 +276,7 @@ public class RestClient implements ClientHttpRequestInterceptor {
}
public Asset assignAsset(CustomerId customerId, AssetId assetId) {
- return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class,
+ return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", HttpEntity.EMPTY, Asset.class,
customerId.toString(), assetId.toString()).getBody();
}
@@ -202,6 +288,29 @@ public class RestClient implements ClientHttpRequestInterceptor {
return restTemplate.postForEntity(baseURL + "/api/relation", relation, EntityRelation.class).getBody();
}
+ public Dashboard createDashboard(Dashboard dashboard) {
+ return restTemplate.postForEntity(baseURL + "/api/dashboard", dashboard, Dashboard.class).getBody();
+ }
+
+ public void deleteDashboard(DashboardId dashboardId) {
+ restTemplate.delete(baseURL + "/api/dashboard/{dashboardId}", dashboardId);
+ }
+
+ public List findTenantDashboards() {
+ try {
+ ResponseEntity> dashboards =
+ restTemplate.exchange(baseURL + "/api/tenant/dashboards?limit=100000", HttpMethod.GET, null, new ParameterizedTypeReference>() {
+ });
+ return dashboards.getBody().getData();
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Collections.emptyList();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
public DeviceCredentials getCredentials(DeviceId id) {
return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
}
@@ -210,14 +319,1702 @@ public class RestClient implements ClientHttpRequestInterceptor {
return restTemplate;
}
- public String getToken() {
- return token;
+ public Optional getAdminSettings(String key) {
+ try {
+ ResponseEntity adminSettings = restTemplate.getForEntity(baseURL + "/api/admin/settings/{key}", AdminSettings.class, key);
+ return Optional.ofNullable(adminSettings.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
}
- @Override
- public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
- HttpRequest wrapper = new HttpRequestWrapper(request);
- wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
- return execution.execute(wrapper, bytes);
+ public AdminSettings saveAdminSettings(AdminSettings adminSettings) {
+ return restTemplate.postForEntity(baseURL + "/api/admin/settings", adminSettings, AdminSettings.class).getBody();
+ }
+
+ public void sendTestMail(AdminSettings adminSettings) {
+ restTemplate.postForEntity(baseURL + "/api/admin/settings/testMail", adminSettings, AdminSettings.class);
+ }
+
+ public Optional getSecuritySettings() {
+ try {
+ ResponseEntity securitySettings = restTemplate.getForEntity(baseURL + "/api/admin/securitySettings", SecuritySettings.class);
+ return Optional.ofNullable(securitySettings.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public SecuritySettings saveSecuritySettings(SecuritySettings securitySettings) {
+ return restTemplate.postForEntity(baseURL + "/api/admin/securitySettings", securitySettings, SecuritySettings.class).getBody();
+ }
+
+ public Optional checkUpdates() {
+ try {
+ ResponseEntity updateMsg = restTemplate.getForEntity(baseURL + "/api/admin/updates", UpdateMessage.class);
+ return Optional.ofNullable(updateMsg.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional getAlarmById(String alarmId) {
+ try {
+ ResponseEntity alarm = restTemplate.getForEntity(baseURL + "/api/alarm/{alarmId}", Alarm.class, alarmId);
+ return Optional.ofNullable(alarm.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional getAlarmInfoById(String alarmId) {
+ try {
+ ResponseEntity alarmInfo = restTemplate.getForEntity(baseURL + "/api/alarm/info/{alarmId}", AlarmInfo.class, alarmId);
+ return Optional.ofNullable(alarmInfo.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Alarm saveAlarm(Alarm alarm) {
+ return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
+ }
+
+ public void deleteAlarm(String alarmId) {
+ restTemplate.delete(baseURL + "/api/alarm/{alarmId}", alarmId);
+ }
+
+ public void ackAlarm(String alarmId) {
+ restTemplate.postForObject(baseURL + "/api/alarm/{alarmId}/ack", new Object(), Object.class, alarmId);
+ }
+
+ public void clearAlarm(String alarmId) {
+ restTemplate.postForObject(baseURL + "/api/alarm/{alarmId}/clear", new Object(), Object.class, alarmId);
+ }
+
+ public TimePageData getAlarms(String entityType, String entityId, String searchStatus, String status, TimePageLink pageLink, Boolean fetchOriginator) {
+ Map params = new HashMap<>();
+ params.put("entityType", entityType);
+ params.put("entityId", entityId);
+ params.put("searchStatus", searchStatus);
+ params.put("status", status);
+ params.put("fetchOriginator", String.valueOf(fetchOriginator));
+ addPageLinkToParam(params, pageLink);
+
+ String urlParams = getUrlParams(pageLink);
+ return restTemplate.exchange(
+ baseURL + "/api/alarm/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}&fetchOriginator={fetchOriginator}&" + getUrlParams(pageLink),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ }, params).getBody();
+ }
+
+ private String getUrlParams(TimePageLink pageLink) {
+ String urlParams = "limit={limit}&ascOrder={ascOrder}";
+ if (pageLink.getStartTime() != null) {
+ urlParams += "&startTime={startTime}";
+ }
+ if (pageLink.getEndTime() != null) {
+ urlParams += "&endTime={endTime}";
+ }
+ if (pageLink.getIdOffset() != null) {
+ urlParams += "&offset={offset}";
+ }
+ return urlParams;
+ }
+
+ private String getUrlParams(TextPageLink pageLink) {
+ String urlParams = "limit={limit}";
+ if (!isEmpty(pageLink.getTextSearch())) {
+ urlParams += "&textSearch={textSearch}";
+ }
+ if (!isEmpty(pageLink.getIdOffset())) {
+ urlParams += "&idOffset={idOffset}";
+ }
+ if (!isEmpty(pageLink.getTextOffset())) {
+ urlParams += "&textOffset={textOffset}";
+ }
+ return urlParams;
+ }
+
+ public Optional getHighestAlarmSeverity(String entityType, String entityId, String searchStatus, String status) {
+ Map params = new HashMap<>();
+ params.put("entityType", entityType);
+ params.put("entityId", entityId);
+ params.put("searchStatus", searchStatus);
+ params.put("status", status);
+ try {
+ ResponseEntity alarmSeverity = restTemplate.getForEntity(baseURL + "/api/alarm/highestSeverity/{entityType}/{entityId}?searchStatus={searchStatus}&status={status}", AlarmSeverity.class, params);
+ return Optional.ofNullable(alarmSeverity.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional getAssetById(String assetId) {
+ try {
+ ResponseEntity asset = restTemplate.getForEntity(baseURL + "/api/asset/{assetId}", Asset.class, assetId);
+ return Optional.ofNullable(asset.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Asset saveAsset(Asset asset) {
+ return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
+ }
+
+ public void deleteAsset(String assetId) {
+ restTemplate.delete(baseURL + "/api/asset/{assetId}", assetId);
+ }
+
+ public Optional assignAssetToCustomer(String customerId,
+ String assetId) {
+ Map params = new HashMap<>();
+ params.put("customerId", customerId);
+ params.put("assetId", assetId);
+
+ try {
+ ResponseEntity asset = restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class, params);
+ return Optional.ofNullable(asset.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional unassignAssetFromCustomer(String assetId) {
+ try {
+ ResponseEntity asset = restTemplate.exchange(baseURL + "/api/customer/asset/{assetId}", HttpMethod.DELETE, HttpEntity.EMPTY, Asset.class, assetId);
+ return Optional.ofNullable(asset.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional assignAssetToPublicCustomer(String assetId) {
+ try {
+ ResponseEntity asset = restTemplate.postForEntity(baseURL + "/api/customer/public/asset/{assetId}", null, Asset.class, assetId);
+ return Optional.ofNullable(asset.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public TextPageData getTenantAssets(TextPageLink pageLink, String type) {
+ Map params = new HashMap<>();
+ params.put("type", type);
+ addPageLinkToParam(params, pageLink);
+
+ ResponseEntity> assets = restTemplate.exchange(
+ baseURL + "/api/tenant/assets?type={type}&" + getUrlParams(pageLink),
+ HttpMethod.GET, HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ params);
+ return assets.getBody();
+ }
+
+ public Optional getTenantAsset(String assetName) {
+ try {
+ ResponseEntity asset = restTemplate.getForEntity(baseURL + "/api/tenant/assets?assetName={assetName}", Asset.class, assetName);
+ return Optional.ofNullable(asset.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public TextPageData getCustomerAssets(String customerId, TextPageLink pageLink, String type) {
+ Map params = new HashMap<>();
+ params.put("customerId", customerId);
+ params.put("type", type);
+ addPageLinkToParam(params, pageLink);
+
+ ResponseEntity> assets = restTemplate.exchange(
+ baseURL + "/api/customer/{customerId}/assets?type={type}&" + getUrlParams(pageLink),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ params);
+ return assets.getBody();
+ }
+
+ public List getAssetsByIds(String[] assetIds) {
+ return restTemplate.exchange(
+ baseURL + "/api/assets?assetIds={assetIds}",
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ String.join(",", assetIds)).getBody();
+ }
+
+ public List findByQuery(AssetSearchQuery query) {
+ return restTemplate.exchange(
+ URI.create(baseURL + "/api/assets"),
+ HttpMethod.POST,
+ new HttpEntity<>(query),
+ new ParameterizedTypeReference>() {
+ }).getBody();
+ }
+
+ public List getAssetTypes() {
+ return restTemplate.exchange(URI.create(
+ baseURL + "/api/asset/types"),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ }).getBody();
+ }
+
+ public TimePageData getAuditLogsByCustomerId(String customerId, TimePageLink pageLink, String actionTypes) {
+ Map params = new HashMap<>();
+ params.put("customerId", customerId);
+ params.put("actionTypes", actionTypes);
+ addPageLinkToParam(params, pageLink);
+
+ ResponseEntity> auditLog = restTemplate.exchange(
+ baseURL + "/api/audit/logs/customer/{customerId}?actionTypes={actionTypes}&" + getUrlParams(pageLink),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ params);
+ return auditLog.getBody();
+ }
+
+ public TimePageData getAuditLogsByUserId(String userId, TimePageLink pageLink, String actionTypes) {
+ Map params = new HashMap<>();
+ params.put("userId", userId);
+ params.put("actionTypes", actionTypes);
+ addPageLinkToParam(params, pageLink);
+
+ ResponseEntity> auditLog = restTemplate.exchange(
+ baseURL + "/api/audit/logs/user/{userId}?actionTypes={actionTypes}&" + getUrlParams(pageLink),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ params);
+ return auditLog.getBody();
+ }
+
+ public TimePageData getAuditLogsByEntityId(String entityType, String entityId, String actionTypes, TimePageLink pageLink) {
+ Map params = new HashMap<>();
+ params.put("entityType", entityType);
+ params.put("entityId", entityId);
+ params.put("actionTypes", actionTypes);
+ addPageLinkToParam(params, pageLink);
+
+ ResponseEntity> auditLog = restTemplate.exchange(
+ baseURL + "/api/audit/logs/entity/{entityType}/{entityId}?actionTypes={actionTypes}&" + getUrlParams(pageLink),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ params);
+ return auditLog.getBody();
+ }
+
+ public TimePageData getAuditLogs(TimePageLink pageLink, String actionTypes) {
+ Map params = new HashMap<>();
+ params.put("actionTypes", actionTypes);
+ addPageLinkToParam(params, pageLink);
+
+ ResponseEntity> auditLog = restTemplate.exchange(
+ baseURL + "/api/audit/logs?actionTypes={actionTypes}&" + getUrlParams(pageLink),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ params);
+ return auditLog.getBody();
+ }
+
+ public String getActivateToken(String userId) {
+ String activationLink = getActivationLink(userId);
+ return StringUtils.delete(activationLink, baseURL + ACTIVATE_TOKEN_REGEX);
+ }
+
+ public Optional getUser() {
+ ResponseEntity user = restTemplate.getForEntity(baseURL + "/api/auth/user", User.class);
+ return Optional.ofNullable(user.getBody());
+ }
+
+ public void logout() {
+ restTemplate.exchange(URI.create(baseURL + "/api/auth/logout"), HttpMethod.POST, HttpEntity.EMPTY, Object.class);
+ }
+
+ public void changePassword(String currentPassword, String newPassword) {
+ ObjectNode changePasswordRequest = objectMapper.createObjectNode();
+ changePasswordRequest.put("currentPassword", currentPassword);
+ changePasswordRequest.put("newPassword", newPassword);
+ restTemplate.exchange(URI.create(baseURL + "/api/auth/changePassword"), HttpMethod.POST, new HttpEntity<>(changePasswordRequest), Object.class);
+ }
+
+ public Optional getUserPasswordPolicy() {
+ try {
+ ResponseEntity userPasswordPolicy = restTemplate.getForEntity(baseURL + "/api/noauth/userPasswordPolicy", UserPasswordPolicy.class);
+ return Optional.ofNullable(userPasswordPolicy.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public ResponseEntity checkActivateToken(String userId) {
+ String activateToken = getActivateToken(userId);
+ return restTemplate.getForEntity(baseURL + "/api/noauth/activate?activateToken={activateToken}", String.class, activateToken);
+ }
+
+ public void requestResetPasswordByEmail(String email) {
+ ObjectNode resetPasswordByEmailRequest = objectMapper.createObjectNode();
+ resetPasswordByEmailRequest.put("email", email);
+ restTemplate.exchange(URI.create(baseURL + "/api/noauth/resetPasswordByEmail"), HttpMethod.POST, new HttpEntity<>(resetPasswordByEmailRequest), Object.class);
+ }
+
+ public Optional activateUser(String userId, String password) {
+ ObjectNode activateRequest = objectMapper.createObjectNode();
+ activateRequest.put("activateToken", getActivateToken(userId));
+ activateRequest.put("password", password);
+ try {
+ ResponseEntity jsonNode = restTemplate.postForEntity(baseURL + "/api/noauth/activate", activateRequest, JsonNode.class);
+ return Optional.ofNullable(jsonNode.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional getComponentDescriptorByClazz(String componentDescriptorClazz) {
+ try {
+ ResponseEntity componentDescriptor = restTemplate.getForEntity(baseURL + "/api/component/{componentDescriptorClazz}", ComponentDescriptor.class, componentDescriptorClazz);
+ return Optional.ofNullable(componentDescriptor.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public List getComponentDescriptorsByType(String componentType) {
+ return restTemplate.exchange(
+ baseURL + "/api/components?componentType={componentType}",
+ HttpMethod.GET, HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ componentType).getBody();
+ }
+
+ public List getComponentDescriptorsByTypes(String[] componentTypes) {
+ return restTemplate.exchange(
+ baseURL + "/api/components?componentTypes={componentTypes}",
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ String.join(",", componentTypes)).getBody();
+ }
+
+ public Optional getCustomerById(String customerId) {
+ try {
+ ResponseEntity customer = restTemplate.getForEntity(baseURL + "/api/customer/{customerId}", Customer.class, customerId);
+ return Optional.ofNullable(customer.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional getShortCustomerInfoById(String customerId) {
+ try {
+ ResponseEntity customerInfo = restTemplate.getForEntity(baseURL + "/api/customer/{customerId}/shortInfo", JsonNode.class, customerId);
+ return Optional.ofNullable(customerInfo.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public String getCustomerTitleById(String customerId) {
+ return restTemplate.getForObject(baseURL + "/api/customer/{customerId}/title", String.class, customerId);
+ }
+
+ public Customer saveCustomer(Customer customer) {
+ return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
+ }
+
+ public void deleteCustomer(String customerId) {
+ restTemplate.delete(baseURL + "/api/customer/{customerId}", customerId);
+ }
+
+ public TextPageData getCustomers(TextPageLink pageLink) {
+ Map params = new HashMap<>();
+ addPageLinkToParam(params, pageLink);
+
+ ResponseEntity> customer = restTemplate.exchange(
+ baseURL + "/api/customers?" + getUrlParams(pageLink),
+ HttpMethod.GET,
+ HttpEntity.EMPTY,
+ new ParameterizedTypeReference>() {
+ },
+ params);
+ return customer.getBody();
+ }
+
+ public Optional getTenantCustomer(String customerTitle) {
+ try {
+ ResponseEntity customer = restTemplate.getForEntity(baseURL + "/api/tenant/customers?customerTitle={customerTitle}", Customer.class, customerTitle);
+ return Optional.ofNullable(customer.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Long getServerTime() {
+ return restTemplate.getForObject(baseURL + "/api/dashboard/serverTime", Long.class);
+ }
+
+ public Long getMaxDatapointsLimit() {
+ return restTemplate.getForObject(baseURL + "/api/dashboard/maxDatapointsLimit", Long.class);
+ }
+
+ public Optional getDashboardInfoById(String dashboardId) {
+ try {
+ ResponseEntity dashboardInfo = restTemplate.getForEntity(baseURL + "/api/dashboard/info/{dashboardId}", DashboardInfo.class, dashboardId);
+ return Optional.ofNullable(dashboardInfo.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Optional getDashboardById(String dashboardId) {
+ try {
+ ResponseEntity dashboard = restTemplate.getForEntity(baseURL + "/api/dashboard/{dashboardId}", Dashboard.class, dashboardId);
+ return Optional.ofNullable(dashboard.getBody());
+ } catch (HttpClientErrorException exception) {
+ if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
+ return Optional.empty();
+ } else {
+ throw exception;
+ }
+ }
+ }
+
+ public Dashboard saveDashboard(Dashboard dashboard) {
+ return restTemplate.postForEntity(baseURL + "/api/dashboard", dashboard, Dashboard.class).getBody();
+ }
+
+ public void deleteDashboard(String dashboardId) {
+ restTemplate.delete(baseURL + "/api/dashboard/{dashboardId}", dashboardId);
+ }
+
+ public Optional assignDashboardToCustomer(String customerId, String dashboardId) {
+ try {
+ ResponseEntity