thingsboard-developers

Changes

docker/kafka.env 2(+1 -1)

Details

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 a352a94..e7f0acf 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
@@ -13,9 +13,9 @@
         "sizeX": 7.5,
         "sizeY": 3.5,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           maxlength=\"{{settings.maxLength}}\"\n                           minlength=\"{{settings.minLength}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n        \n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           maxlength=\"{{settings.maxLength}}\"\n                           minlength=\"{{settings.minLength}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n        \n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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\n\r\nself.onInit = function() {\r\n\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    settings = self.ctx.settings || {};\r\n    $scope.settings = settings;\r\n    $scope.isValidParameter = true;\r\n    $scope.dataKeyDetected = false;\r\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\r\n    $scope.labelValue = settings.labelValue || \"Value\";\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 === '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) {\r\n            if (datasource.dataKeys[0].type != \"attribute\") {\r\n                $scope.isValidParameter = false;\r\n            } else {\r\n                $scope.currentKey = datasource.dataKeys[0].name;\r\n                $scope.dataKeyType = datasource.dataKeys[0].type;\r\n                $scope.dataKeyDetected = true;\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                    {\r\n                        key: $scope.currentKey,\r\n                        value: $scope.currentValue\r\n                    }\r\n                ]\r\n            ).then(\r\n                function success() {\r\n                    $scope.originalValue = $scope.currentValue;\r\n                    if (settings.showResultMessage) {\r\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n                    }\r\n                },\r\n                function fail() {\r\n                    if (settings.showResultMessage) {\r\n                        toast.showError('Update failed', 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.currentValue === $scope.originalValue) {\r\n            $scope.isFocused = false;\r\n        }\r\n    }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n    try {\r\n        if ($scope.dataKeyDetected) {\r\n            if (!$scope.isFocused) {\r\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n                $scope.$digest();\r\n            }\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\r\n}\r\n\r\nself.typeParameters = function() {\r\n    return {\r\n        maxDatasources: 1,\r\n        maxDataKeys: 1,\r\n        dataKeysOptional: true\r\n    }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\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\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    settings = self.ctx.settings || {};\r\n    $scope.settings = settings;\r\n    $scope.isValidParameter = true;\r\n    $scope.dataKeyDetected = false;\r\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\r\n    $scope.labelValue = settings.labelValue || \"Value\";\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 === '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) {\r\n            if (datasource.dataKeys[0].type != \"attribute\") {\r\n                $scope.isValidParameter = false;\r\n            } else {\r\n                $scope.currentKey = datasource.dataKeys[0].name;\r\n                $scope.dataKeyType = datasource.dataKeys[0].type;\r\n                $scope.dataKeyDetected = true;\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                    {\r\n                        key: $scope.currentKey,\r\n                        value: $scope.currentValue\r\n                    }\r\n                ]\r\n            ).then(\r\n                function success() {\r\n                    $scope.originalValue = $scope.currentValue;\r\n                    if (settings.showResultMessage) {\r\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n                    }\r\n                },\r\n                function fail() {\r\n                    if (settings.showResultMessage) {\r\n                        toast.showError('Update failed', 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.currentValue === $scope.originalValue) {\r\n            $scope.isFocused = false;\r\n        }\r\n    }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n    try {\r\n        if ($scope.dataKeyDetected) {\r\n            if (!$scope.isFocused) {\r\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\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\r\n}\r\n\r\nself.typeParameters = function() {\r\n    return {\r\n        maxDatasources: 1,\r\n        maxDataKeys: 1\r\n    }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxLength\": {\n                \"title\": \"Max length\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minLength\": {\n                \"title\": \"Min length\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxLength\",\n        \"minLength\"\n    ]\n}",
         "dataKeySettingsSchema": "{}\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 server string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -31,7 +31,7 @@
         "resources": [],
         "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = self.ctx.settings || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.server.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                   if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            if (!$scope.isFocused) {\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n                correctValue($scope.currentValue);\n                $scope.$digest();\n            }\n        }\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.server.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            if (!$scope.isFocused) {\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n                correctValue($scope.currentValue);\n                $scope.$digest();\n            }\n        }\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxValue\": {\n                \"title\": \"Max value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minValue\": {\n                \"title\": \"Min value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxValue\",\n        \"minValue\"\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 integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -45,9 +45,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           step=\"any\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           step=\"any\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = self.ctx.settings || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys[0].type != \"attribute\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.server.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                     if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n        if (!$scope.isFocused) {\n            $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n            correctValue($scope.currentValue);\n            $scope.$digest();\n        }\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.server.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            if (!$scope.isFocused) {\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n                correctValue($scope.currentValue);\n                $scope.$digest();\n            }\n        }\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxValue\": {\n                \"title\": \"Max value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minValue\": {\n                \"title\": \"Min value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxValue\",\n        \"minValue\"\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 double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -61,9 +61,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-checkbox ng-model=\"checkboxValue\"\n                             aria-label=\"Switch entity attribute value\"\n                             ng-change=\"changed()\"\n                             ng-true-value=\"'true'\"\n                             ng-false-value=\"'false'\"\n                >\n                    {{currentValue}}\n                </md-checkbox>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-checkbox ng-model=\"checkboxValue\"\n                             aria-label=\"Switch entity attribute value\"\n                             ng-change=\"changed()\"\n                             ng-true-value=\"'true'\"\n                             ng-false-value=\"'false'\"\n                >\n                    {{currentValue}}\n                </md-checkbox>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\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 !important;\n    line-height: 20px;\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\nmd-toast{\n    min-width: 0;\n}\nmd-toast .md-toast-content {\n    font-size: 14px!important;\n}",
-        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.message = 'No entity selected';\n\n    $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n    $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n    settings.trueValue = settings.trueValue || true;\n    settings.falseValue = settings.falseValue || false;\n    \n    map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n    mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n    $scope.checkboxValue = \"false\";\n    $scope.currentValue = map[$scope.checkboxValue];\n\n    $scope.changed = function () {\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.updateAttribute();\n    }\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys[0].type != \"attribute\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.server.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n        $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.$digest();\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.message = 'No entity selected';\n\n    settings.trueValue = settings.trueValue || true;\n    settings.falseValue = settings.falseValue || false;\n    \n    map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n    mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n    $scope.checkboxValue = \"false\";\n    $scope.currentValue = map[$scope.checkboxValue];\n\n    $scope.changed = function () {\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.updateAttribute();\n    }\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.server.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n            $scope.currentValue = map[$scope.checkboxValue];\n            $scope.$digest();\n        }\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"trueValue\": {\n                \"title\": \"True value\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"falseValue\": {\n                \"title\": \"False value\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"trueValue\",\n        \"falseValue\"\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 boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -77,9 +77,9 @@
         "sizeX": 7.5,
         "sizeY": 3.5,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           maxlength=\"{{settings.maxLength}}\"\n                           minlength=\"{{settings.minLength}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update shared attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update shared attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\n             ng-hide=\"entityDetected\"\n             ng-bind=\"message\"\n        ></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           maxlength=\"{{settings.maxLength}}\"\n                           minlength=\"{{settings.minLength}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update shared attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update shared attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\n             ng-hide=\"entityDetected\"\n             ng-bind=\"message\"\n        ></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = self.ctx.settings || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.message = 'No entity selected';\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have shared attributes';\n            }\n        }\n        if (datasource.dataKeys[0].type != \"attribute\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n        if (!$scope.isFocused) {\n            $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n            $scope.$digest();\n        }\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.message = 'No entity selected';\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have shared attributes';\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            if (!$scope.isFocused) {\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n                $scope.$digest();\n            }\n        }\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxLength\": {\n                \"title\": \"Max length\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minLength\": {\n                \"title\": \"Min length\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxLength\",\n        \"minLength\"\n    ]\n}",
         "dataKeySettingsSchema": "{}\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 shared string attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -93,9 +93,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update shared attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update shared attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\n             ng-hide=\"entityDetected\"\n             ng-bind=\"message\"\n        >\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update shared attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update shared attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\"\n             ng-hide=\"entityDetected\"\n             ng-bind=\"message\"\n        >\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = self.ctx.settings || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n    $scope.message = 'No entity selected';\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have share attribute';\n            }\n        }\n        if (datasource.dataKeys[0].type != \"attribute\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n        if (!$scope.isFocused) {\n            $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n            correctValue($scope.currentValue);\n            $scope.$digest();\n        }\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n    $scope.message = 'No entity selected';\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have share attribute';\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            if (!$scope.isFocused) {\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n                correctValue($scope.currentValue);\n                $scope.$digest();\n            }\n        }\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxValue\": {\n                \"title\": \"Max value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minValue\": {\n                \"title\": \"Min value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxValue\",\n        \"minValue\"\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 integer attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -109,9 +109,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           step=\"any\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update shared attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update shared attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           step=\"any\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update shared attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update shared attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = self.ctx.settings || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n    $scope.message = 'No entity selected';\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have shared attributes';\n            }\n        }\n        if (datasource.dataKeys[0].type != \"attribute\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n        if (!$scope.isFocused) {\n            $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n            correctValue($scope.currentValue);\n            $scope.$digest();\n        }\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity attribute is required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n    $scope.message = 'No entity selected';\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have shared attributes';\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            if (!$scope.isFocused) {\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n                correctValue($scope.currentValue);\n                $scope.$digest();\n            }\n        }\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxValue\": {\n                \"title\": \"Max value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minValue\": {\n                \"title\": \"Min value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxValue\",\n        \"minValue\"\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 double attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -125,9 +125,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-checkbox ng-model=\"checkboxValue\"\n                             aria-label=\"Switch entity attribute value\"\n                             ng-change=\"changed()\"\n                             ng-true-value=\"'true'\"\n                             ng-false-value=\"'false'\"\n                >\n                    {{currentValue}}\n                </md-checkbox>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-checkbox ng-model=\"checkboxValue\"\n                             aria-label=\"Switch entity attribute value\"\n                             ng-change=\"changed()\"\n                             ng-true-value=\"'true'\"\n                             ng-false-value=\"'false'\"\n                >\n                    {{currentValue}}\n                </md-checkbox>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No attribute is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Timeseries parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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\nmd-toast{\n    min-width: 0;\n}\nmd-toast .md-toast-content {\n    font-size: 14px!important;\n}",
-        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.message = 'No entity selected';\n\n    $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n    $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n    settings.trueValue = settings.trueValue || true;\n    settings.falseValue = settings.falseValue || false;\n    \n    map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n    mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n    $scope.checkboxValue = \"false\";\n    $scope.currentValue = map[$scope.checkboxValue];\n\n    $scope.changed = function () {\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.updateAttribute();\n    }\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have shared attributes';\n            }\n        }\n        if (datasource.dataKeys[0].type != \"attribute\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n        $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.$digest();\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.message = 'No entity selected';\n\n    settings.trueValue = settings.trueValue || true;\n    settings.falseValue = settings.falseValue || false;\n    \n    map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n    mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n    $scope.checkboxValue = \"false\";\n    $scope.currentValue = map[$scope.checkboxValue];\n\n    $scope.changed = function () {\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.updateAttribute();\n    }\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity cannot have shared attributes';\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"attribute\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            attributeService.saveEntityAttributes(\n                datasource.entityType,\n                datasource.entityId,\n                types.attributesScope.shared.value,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.$digest();\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"trueValue\": {\n                \"title\": \"True value\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"falseValue\": {\n                \"title\": \"False value\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"trueValue\",\n        \"falseValue\"\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\":{\"trueValue\":\"active\",\"falseValue\":\"inactive\"},\"title\":\"Update shared boolean attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -141,9 +141,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           maxlength=\"{{settings.maxLength}}\"\n                           minlength=\"{{settings.minLength}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n        \n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           maxlength=\"{{settings.maxLength}}\"\n                           minlength=\"{{settings.minLength}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update string timeseries</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n        \n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No timeseries is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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 $q\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\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    $q = $scope.$injector.get('$q');\r\n    $http = $scope.$injector.get('$http');\r\n    settings = self.ctx.settings || {};\r\n    $scope.settings = settings;\r\n    $scope.isValidParameter = true;\r\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\r\n    $scope.labelValue = settings.labelValue || \"Value\";\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 === '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[0].type != \"timeseries\") {\r\n            $scope.isValidParameter = false;\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            saveEntityTimeseries(\r\n                datasource.entityType,\r\n                datasource.entityId,\r\n                [\r\n                    {\r\n                        key: $scope.currentKey,\r\n                        value: $scope.currentValue\r\n                    }\r\n                ]\r\n            ).then(\r\n                function success() {\r\n                    $scope.originalValue = $scope.currentValue;\r\n                    if (settings.showResultMessage) {\r\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n                    }\r\n                },\r\n                function fail() {\r\n                    if (settings.showResultMessage) {\r\n                        toast.showError('Update failed', 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.currentValue === $scope.originalValue) {\r\n            $scope.isFocused = false;\r\n        }\r\n    }\r\n\r\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n        var deferred = $q.defer();\r\n        var telemetriesData = {};\r\n        for (var a = 0; a < telemetries.length; a++) {\r\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n            }\r\n        }\r\n        if (Object.keys(telemetriesData).length) {\r\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n            $http.post(url, telemetriesData).then(\r\n                function(response) {\r\n                    deferred.resolve(response.data);\r\n                },\r\n                function() {\r\n                    deferred.reject();\r\n                }\r\n            );\r\n        }\r\n        return deferred.promise;\r\n    }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n    try {\r\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\r\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\r\n\r\n        if (!$scope.isFocused) {\r\n            $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\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\r\n}\r\n\r\nself.typeParameters = function() {\r\n    return {\r\n        maxDatasources: 1,\r\n        maxDataKeys: 1,\r\n        dataKeysOptional: true\r\n    }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n",
+        "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $q\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\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    $q = $scope.$injector.get('$q');\r\n    $http = $scope.$injector.get('$http');\r\n    settings = self.ctx.settings || {};\r\n    $scope.settings = settings;\r\n    $scope.isValidParameter = true;\r\n    $scope.dataKeyDetected = false;\r\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\r\n    $scope.labelValue = settings.labelValue || \"Value\";\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 === '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) {\r\n            if (datasource.dataKeys[0].type != \"timeseries\") {\r\n                $scope.isValidParameter = false;\r\n            } else {\r\n                $scope.currentKey = datasource.dataKeys[0].name;\r\n                $scope.dataKeyType = datasource.dataKeys[0].type;\r\n                $scope.dataKeyDetected = true;\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            saveEntityTimeseries(\r\n                datasource.entityType,\r\n                datasource.entityId,\r\n                [\r\n                    {\r\n                        key: $scope.currentKey,\r\n                        value: $scope.currentValue\r\n                    }\r\n                ]\r\n            ).then(\r\n                function success() {\r\n                    $scope.originalValue = $scope.currentValue;\r\n                    if (settings.showResultMessage) {\r\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n                    }\r\n                },\r\n                function fail() {\r\n                    if (settings.showResultMessage) {\r\n                        toast.showError('Update failed', 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.currentValue === $scope.originalValue) {\r\n            $scope.isFocused = false;\r\n        }\r\n    }\r\n\r\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n        var deferred = $q.defer();\r\n        var telemetriesData = {};\r\n        for (var a = 0; a < telemetries.length; a++) {\r\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n            }\r\n        }\r\n        if (Object.keys(telemetriesData).length) {\r\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n            $http.post(url, telemetriesData).then(\r\n                function(response) {\r\n                    deferred.resolve(response.data);\r\n                },\r\n                function() {\r\n                    deferred.reject();\r\n                }\r\n            );\r\n        }\r\n        return deferred.promise;\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                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\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\r\n}\r\n\r\nself.typeParameters = function() {\r\n    return {\r\n        maxDatasources: 1,\r\n        maxDataKeys: 1\r\n    }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxLength\": {\n                \"title\": \"Max length\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minLength\": {\n                \"title\": \"Min length\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxLength\",\n        \"minLength\"\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 string timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -157,9 +157,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-checkbox ng-model=\"checkboxValue\"\n                             aria-label=\"Switch entity attribute value\"\n                             ng-change=\"changed()\"\n                             ng-true-value=\"'true'\"\n                             ng-false-value=\"'false'\"\n                >\n                    {{currentValue}}\n                </md-checkbox>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-checkbox ng-model=\"checkboxValue\"\n                             aria-label=\"Switch entity attribute value\"\n                             ng-change=\"changed()\"\n                             ng-true-value=\"'true'\"\n                             ng-false-value=\"'false'\"\n                >\n                    {{currentValue}}\n                </md-checkbox>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\" ng-bind=\"message\"></div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No timeseries is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "templateCss": ".attribute-update-form {\r\n    overflow: hidden;\r\n    height: 100%;\r\n    display: flex;\r\n    flex-direction: column;\r\n}\r\n\r\n.entity-title {\r\n    font-weight: bold;\r\n    font-size: 22px;\r\n    padding-top: 12px;\r\n    padding-bottom: 6px;\r\n    color: #666;\r\n}\r\n\r\n.attribute-update-form__grid {\r\n    display: flex;\r\n}\r\n.grid__element:first-child {\r\n    flex: 1;\r\n}\r\n\r\n.grid__element {\r\n    display: flex;\r\n}\r\n\r\n.attribute-update-form .md-button.md-icon-button {\r\n    margin: 0;\r\n}\r\n\r\n.attribute-update-form .md-button.md-icon-button {\r\n    width: 32px;\r\n    min-width: 32px;\r\n    height: 32px;\r\n    min-height: 32px;\r\n    padding: 0 !important;\r\n    margin: 0 !important;\r\n    line-height: 20px;\r\n}\r\n\r\n.attribute-update-form .md-icon-button md-icon {\r\n    width: 20px;\r\n    min-width: 20px;\r\n    height: 20px;\r\n    min-height: 20px;\r\n    font-size: 20px;\r\n}\r\n\r\n\r\nmd-toast{\r\n    min-width: 0;\r\n}\r\nmd-toast .md-toast-content {\r\n    font-size: 14px!important;\r\n}",
-        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $q\nlet $http;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    $q = $scope.$injector.get('$q');\n    $http = $scope.$injector.get('$http');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n\n    $scope.message = 'No entity selected';\n\n    $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n    $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n    settings.trueValue = settings.trueValue || true;\n    settings.falseValue = settings.falseValue || false;\n    \n    map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n    mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n    $scope.checkboxValue = \"false\";\n    $scope.currentValue = map[$scope.checkboxValue];\n\n    $scope.changed = function () {\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.updateAttribute();\n    }\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType === \"DEVICE\") {\n                if (datasource.entityType && datasource.entityId) {\n                    $scope.entityName = datasource.entityName;\n                    if (settings.widgetTitle && settings.widgetTitle.length) {\n                        $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                    } else {\n                        $scope.titleTemplate = self.ctx.widgetConfig.title;\n                    }\n\n                    $scope.entityDetected = true;\n                }\n            } else {\n                $scope.message = 'Selected entity can not have share attribute';\n            }\n        }\n        if (datasource.dataKeys[0].type != \"timeseries\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            saveEntityTimeseries(\n                datasource.entityType,\n                datasource.entityId,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\n        var deferred = $q.defer();\n        var telemetriesData = {};\n        for (var a = 0; a < telemetries.length; a++) {\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\n            }\n        }\n        if (Object.keys(telemetriesData).length) {\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n            $http.post(url, telemetriesData).then(\n                function(response) {\n                    deferred.resolve(response.data);\n                },\n                function() {\n                    deferred.reject();\n                }\n            );\n        }\n        return deferred.promise;\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n        $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.$digest();\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $q\nlet $http;\nlet map;\nlet mapReverse;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    $q = $scope.$injector.get('$q');\n    $http = $scope.$injector.get('$http');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.message = 'No entity selected';\n\n    settings.trueValue = settings.trueValue || true;\n    settings.falseValue = settings.falseValue || false;\n    \n    map = {\"true\":settings.trueValue, \"false\": settings.falseValue};\n    mapReverse = {[settings.trueValue]:\"true\", [settings.falseValue]:\"false\"};\n    $scope.checkboxValue = \"false\";\n    $scope.currentValue = map[$scope.checkboxValue];\n\n    $scope.changed = function () {\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.updateAttribute();\n    }\n    \n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"timeseries\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            saveEntityTimeseries(\n                datasource.entityType,\n                datasource.entityId,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\n        var deferred = $q.defer();\n        var telemetriesData = {};\n        for (var a = 0; a < telemetries.length; a++) {\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\n            }\n        }\n        if (Object.keys(telemetriesData).length) {\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n            $http.post(url, telemetriesData).then(\n                function(response) {\n                    deferred.resolve(response.data);\n                },\n                function() {\n                    deferred.reject();\n                }\n            );\n        }\n        return deferred.promise;\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.checkboxValue = mapReverse[$scope.originalValue = self.ctx.data[0].data[0][1]] || 'false';\n        $scope.currentValue = map[$scope.checkboxValue];\n        $scope.$digest();\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"trueValue\": {\n                \"title\": \"True value\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"falseValue\": {\n                \"title\": \"False value\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"trueValue\",\n        \"falseValue\"\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\":{\"trueValue\":\"active\",\"falseValue\":\"inactive\"},\"title\":\"Update boolean timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -173,9 +173,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           step=\"any\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           step=\"any\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No timeseries is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter can not be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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 $q\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\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    $q = $scope.$injector.get('$q');\r\n    $http = $scope.$injector.get('$http');\r\n    settings = self.ctx.settings || {};\r\n    $scope.settings = settings;\r\n    $scope.isValidParameter = true;\r\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\r\n    $scope.labelValue = settings.labelValue || \"Value\";\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 === '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[0].type != \"timeseries\") {\r\n            $scope.isValidParameter = false;\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            saveEntityTimeseries(\r\n                datasource.entityType,\r\n                datasource.entityId,\r\n                [\r\n                    {\r\n                        key: $scope.currentKey,\r\n                        value: $scope.currentValue\r\n                    }\r\n                ]\r\n            ).then(\r\n                function success() {\r\n                    $scope.originalValue = $scope.currentValue;\r\n                    if (settings.showResultMessage) {\r\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n                    }\r\n                },\r\n                function fail() {\r\n                    if (settings.showResultMessage) {\r\n                        toast.showError('Update failed', 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.currentValue === $scope.originalValue) {\r\n            $scope.isFocused = false;\r\n        }\r\n    }\r\n\r\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n        var deferred = $q.defer();\r\n        var telemetriesData = {};\r\n        for (var a = 0; a < telemetries.length; a++) {\r\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n            }\r\n        }\r\n        if (Object.keys(telemetriesData).length) {\r\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n            $http.post(url, telemetriesData).then(\r\n                function(response) {\r\n                    deferred.resolve(response.data);\r\n                },\r\n                function() {\r\n                    deferred.reject();\r\n                }\r\n            );\r\n        }\r\n        return deferred.promise;\r\n    }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n    try {\r\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\r\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\r\n\r\n        if (!$scope.isFocused) {\r\n            $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n            correctValue($scope.currentValue);\r\n            $scope.$digest();\r\n        }\r\n\r\n    } catch (e) {\r\n        console.log(e);\r\n    }\r\n}\r\n\r\nfunction correctValue(value) {\r\n    if (typeof value !== \"number\") {\r\n        $scope.currentValue = 0;\r\n    }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n    return {\r\n        maxDatasources: 1,\r\n        maxDataKeys: 1,\r\n        dataKeysOptional: true\r\n    }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\r\n}\r\n",
+        "controllerScript": "let $scope;\r\nlet settings;\r\nlet attributeService;\r\nlet toast;\r\nlet utils;\r\nlet types;\r\nlet $q;\r\nlet $http;\r\n\r\nself.onInit = function() {\r\n\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    $q = $scope.$injector.get('$q');\r\n    $http = $scope.$injector.get('$http');\r\n    settings = angular.copy(self.ctx.settings) || {};\r\n    $scope.settings = settings;\r\n    $scope.isValidParameter = true;\r\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\r\n    $scope.labelValue = settings.labelValue || \"Value\";\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 === '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) {\r\n            if (datasource.dataKeys[0].type != \"timeseries\") {\r\n                $scope.isValidParameter = false;\r\n            } else {\r\n                $scope.currentKey = datasource.dataKeys[0].name;\r\n                $scope.dataKeyType = datasource.dataKeys[0].type;\r\n                $scope.dataKeyDetected = true;\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            saveEntityTimeseries(\r\n                datasource.entityType,\r\n                datasource.entityId,\r\n                [\r\n                    {\r\n                        key: $scope.currentKey,\r\n                        value: $scope.currentValue\r\n                    }\r\n                ]\r\n            ).then(\r\n                function success() {\r\n                    $scope.originalValue = $scope.currentValue;\r\n                    if (settings.showResultMessage) {\r\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\r\n                    }\r\n                },\r\n                function fail() {\r\n                    if (settings.showResultMessage) {\r\n                        toast.showError('Update failed', 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.currentValue === $scope.originalValue) {\r\n            $scope.isFocused = false;\r\n        }\r\n    }\r\n\r\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\r\n        var deferred = $q.defer();\r\n        var telemetriesData = {};\r\n        for (var a = 0; a < telemetries.length; a++) {\r\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\r\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\r\n            }\r\n        }\r\n        if (Object.keys(telemetriesData).length) {\r\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\r\n            $http.post(url, telemetriesData).then(\r\n                function(response) {\r\n                    deferred.resolve(response.data);\r\n                },\r\n                function() {\r\n                    deferred.reject();\r\n                }\r\n            );\r\n        }\r\n        return deferred.promise;\r\n    }\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n\r\n    try {\r\n        if ($scope.dataKeyDetected) {\r\n            if (!$scope.isFocused) {\r\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\r\n                correctValue($scope.currentValue);\r\n                $scope.$digest();\r\n            }\r\n        }\r\n    } catch (e) {\r\n        console.log(e);\r\n    }\r\n}\r\n\r\nfunction correctValue(value) {\r\n    if (typeof value !== \"number\") {\r\n        $scope.currentValue = 0;\r\n    }\r\n}\r\n\r\nself.onResize = function() {\r\n\r\n}\r\n\r\nself.typeParameters = function() {\r\n    return {\r\n        maxDatasources: 1,\r\n        maxDataKeys: 1\r\n    }\r\n}\r\n\r\nself.onDestroy = function() {\r\n\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxValue\": {\n                \"title\": \"Max value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minValue\": {\n                \"title\": \"Min value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxValue\",\n        \"minValue\"\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 double timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
@@ -189,9 +189,9 @@
         "sizeX": 7.5,
         "sizeY": 3,
         "resources": [],
-        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
+        "templateHtml": "<form class=\"attribute-update-form\"\n      name=\"attrUpdateForm\"\n      ng-submit=\"updateAttribute($event)\"\n>\n    <div style=\"padding: 0 8px; margin: auto 0;\">\n\n        <div class=\"attribute-update-form__grid\" ng-show=\"entityDetected && isValidParameter && dataKeyDetected\">\n            <div class=\"grid__element\">\n                <md-input-container ng-class=\"{'show-label': settings.showLabel}\" class=\"md-block\" style=\"width: 100%;\">\n                    <label>{{labelValue}}</label>\n                    <input required\n                           name=\"attribute\"\n                           ng-model=\"currentValue\"\n                           ng-focus=\"isFocused = true\"\n                           ng-blur=\"changeFocus()\"\n                           type=\"number\"\n                           max=\"{{settings.maxValue}}\"\n                           min=\"{{settings.minValue}}\"\n                    >\n                    <div ng-messages=\"attrUpdateForm.attribute.$error\">\n                        <div ng-message=\"required\">{{requiredErrorMessage}}</div>\n                    </div>\n                </md-input-container>\n            </div>\n\n            <div class=\"grid__element\">\n                <md-button class=\"md-icon-button applyChanges\"\n                           aria-label=\"Update server attribute\"\n                           type=\"submit\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"isFocused = false\"\n                >\n                    <md-icon>check</md-icon>\n                    <md-tooltip md-direction=\"top\">Update server attribute</md-tooltip>\n                </md-button>\n                <md-button class=\"md-icon-button discardChanges\"\n                           aria-label=\"Discard changes\"\n                           ng-disabled=\"originalValue === currentValue\"\n                           ng-click=\"currentValue = originalValue; isFocused = false\"\n                >\n                    <md-icon>close</md-icon>\n                    <md-tooltip md-direction=\"top\">Discard changes</md-tooltip>\n                </md-button>\n            </div>\n        </div>\n\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-hide=\"entityDetected\">\n            No entity selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !dataKeyDetected\">\n            No timeseries is selected\n        </div>\n        <div style=\"text-align: center; font-size: 18px; color: #a0a0a0;\" ng-show=\"entityDetected && !isValidParameter\">\n            Attribute parameter cannot be used in this widget\n        </div>\n    </div>\n</form>",
         "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: 1;\n}\n.grid__element:last-child {\n    margin-top: 19px;\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 !important;\n    line-height: 20px;\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.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;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $q;\nlet $http;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    $q = $scope.$injector.get('$q');\n    $http = $scope.$injector.get('$http');\n    settings = self.ctx.settings || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys[0].type != \"timeseries\") {\n            $scope.isValidParameter = false;\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            saveEntityTimeseries(\n                datasource.entityType,\n                datasource.entityId,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\n        var deferred = $q.defer();\n        var telemetriesData = {};\n        for (var a = 0; a < telemetries.length; a++) {\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\n            }\n        }\n        if (Object.keys(telemetriesData).length) {\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n            $http.post(url, telemetriesData).then(\n                function(response) {\n                    deferred.resolve(response.data);\n                },\n                function() {\n                    deferred.reject();\n                }\n            );\n        }\n        return deferred.promise;\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        $scope.currentKey = self.ctx.datasources[0].dataKeys[0].label;\n        $scope.dataKeyType = self.ctx.datasources[0].dataKeys[0].type;\n\n        if (!$scope.isFocused) {\n            $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n            correctValue($scope.currentValue);\n            $scope.$digest();\n        }\n\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeysOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\n",
+        "controllerScript": "let $scope;\nlet settings;\nlet attributeService;\nlet toast;\nlet utils;\nlet types;\nlet $q;\nlet $http;\n\nself.onInit = function() {\n\n    $scope = self.ctx.$scope;\n    attributeService = $scope.$injector.get('attributeService');\n    toast = $scope.$injector.get('toast');\n    utils = $scope.$injector.get('utils');\n    types = $scope.$injector.get('types');\n    $q = $scope.$injector.get('$q');\n    $http = $scope.$injector.get('$http');\n    settings = angular.copy(self.ctx.settings) || {};\n    $scope.settings = settings;\n    $scope.isValidParameter = true;\n    $scope.dataKeyDetected = false;\n    $scope.requiredErrorMessage = settings.requiredErrorMessage || \"Entity timeseries are required\";\n    $scope.labelValue = settings.labelValue || \"Value\";\n\n    if (self.ctx.datasources && self.ctx.datasources.length) {\n        var datasource = self.ctx.datasources[0];\n        if (datasource.type === 'entity') {\n            if (datasource.entityType && datasource.entityId) {\n                $scope.entityName = datasource.entityName;\n                if (settings.widgetTitle && settings.widgetTitle.length) {\n                    $scope.titleTemplate = utils.customTranslation(settings.widgetTitle, settings.widgetTitle);\n                } else {\n                    $scope.titleTemplate = self.ctx.widgetConfig.title;\n                }\n\n                $scope.entityDetected = true;\n            }\n        }\n        if (datasource.dataKeys.length) {\n            if (datasource.dataKeys[0].type != \"timeseries\") {\n                $scope.isValidParameter = false;\n            } else {\n                $scope.currentKey = datasource.dataKeys[0].name;\n                $scope.dataKeyType = datasource.dataKeys[0].type;\n                $scope.dataKeyDetected = true;\n            }\n        }\n    }\n\n    self.ctx.widgetTitle = utils.createLabelFromDatasource(self.ctx.datasources[0], $scope.titleTemplate);\n\n    $scope.updateAttribute = function () {\n        if ($scope.entityDetected) {\n            var datasource = self.ctx.datasources[0];\n\n            saveEntityTimeseries(\n                datasource.entityType,\n                datasource.entityId,\n                [\n                    {\n                        key: $scope.currentKey,\n                        value: $scope.currentValue\n                    }\n                ]\n            ).then(\n                function success() {\n                    $scope.originalValue = $scope.currentValue;\n                    if (settings.showResultMessage) {\n                        toast.showSuccess('Update successful', 1000, angular.element(self.ctx.$container), 'bottom left');\n                    }\n                },\n                function fail() {\n                    if (settings.showResultMessage) {\n                        toast.showError('Update failed', angular.element(self.ctx.$container), 'bottom left');\n                    }\n                }\n            );\n        }\n    };\n\n    $scope.changeFocus = function () {\n        if ($scope.currentValue === $scope.originalValue) {\n            $scope.isFocused = false;\n        }\n    }\n\n    function saveEntityTimeseries(entityType, entityId, telemetries) {\n        var deferred = $q.defer();\n        var telemetriesData = {};\n        for (var a = 0; a < telemetries.length; a++) {\n            if (angular.isDefined(telemetries[a].value) && telemetries[a].value !== null) {\n                telemetriesData[telemetries[a].key] = telemetries[a].value;\n            }\n        }\n        if (Object.keys(telemetriesData).length) {\n            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/scope';\n            $http.post(url, telemetriesData).then(\n                function(response) {\n                    deferred.resolve(response.data);\n                },\n                function() {\n                    deferred.reject();\n                }\n            );\n        }\n        return deferred.promise;\n    }\n}\n\nself.onDataUpdated = function() {\n\n    try {\n        if ($scope.dataKeyDetected) {\n            if (!$scope.isFocused) {\n                $scope.currentValue = $scope.originalValue = self.ctx.data[0].data[0][1];\n                correctValue($scope.currentValue);\n                $scope.$digest();\n            }\n        }\n    } catch (e) {\n        console.log(e);\n    }\n}\n\nfunction correctValue(value) {\n    if (typeof value !== \"number\") {\n        $scope.currentValue = 0;\n    }\n}\n\nself.onResize = function() {\n\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        maxDataKeys: 1,\n        dataKeyOptional: true\n    }\n}\n\nself.onDestroy = function() {\n\n}\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            \"showLabel\":{\n                \"title\":\"Show label\",\n                \"type\":\"boolean\",\n                \"default\":true\n            },\n            \"labelValue\": {\n                \"title\": \"Label\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"requiredErrorMessage\": {\n                \"title\": \"'Required' error message\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"maxValue\": {\n                \"title\": \"Max value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"minValue\": {\n                \"title\": \"Min value\",\n                \"type\": \"number\",\n                \"default\": \"\"\n            },\n            \"showResultMessage\":{\n                \"title\":\"Show result message\",\n                \"type\":\"boolean\",\n                \"default\":true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"widgetTitle\",\n        \"showResultMessage\",\n        \"showLabel\",\n        \"labelValue\",\n        \"requiredErrorMessage\",\n        \"maxValue\",\n        \"minValue\"\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 integer timeseries\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
diff --git a/application/src/main/data/upgrade/2.2.0/schema_update.sql b/application/src/main/data/upgrade/2.2.0/schema_update.sql
index 1832b79..c97775a 100644
--- a/application/src/main/data/upgrade/2.2.0/schema_update.sql
+++ b/application/src/main/data/upgrade/2.2.0/schema_update.sql
@@ -15,3 +15,5 @@
 --
 
 ALTER TABLE component_descriptor ADD UNIQUE (clazz);
+
+ALTER TABLE entity_view ALTER COLUMN keys SET DATA TYPE varchar(10000000);
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 a59e85c..bd1cd4c 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -56,6 +56,7 @@ import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
 import org.thingsboard.server.dao.user.UserService;
+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;
@@ -276,6 +277,10 @@ public class ActorSystemContext {
     @Setter
     private ActorSystem actorSystem;
 
+    @Autowired
+    @Getter
+    private TbNodeIdProvider nodeIdProvider;
+
     @Getter
     @Setter
     private ActorRef appActor;
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 0baecea..13d3288 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
@@ -162,6 +162,11 @@ class DefaultTbContext implements TbContext {
     }
 
     @Override
+    public String getNodeId() {
+        return mainCtx.getNodeIdProvider().getNodeId();
+    }
+
+    @Override
     public AttributesService getAttributesService() {
         return mainCtx.getAttributesService();
     }
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 69061ba..50949c1 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
@@ -99,7 +99,7 @@ public class DefaultActorService implements ActorService {
 
     @PostConstruct
     public void initActorSystem() {
-        log.info("Initializing Actor system. {}", actorContext.getRuleChainService());
+        log.info("Initializing Actor system.");
         actorContext.setActorService(this);
         system = ActorSystem.create(ACTOR_SYSTEM_NAME, actorContext.getConfig());
         actorContext.setActorSystem(system);
diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
new file mode 100644
index 0000000..5f2cec3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.GenericFilterBean;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.tools.TbRateLimits;
+import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
+import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+@Component
+public class RateLimitProcessingFilter extends GenericFilterBean {
+
+    @Value("${server.rest.limits.tenant.enabled:false}")
+    private boolean perTenantLimitsEnabled;
+    @Value("${server.rest.limits.tenant.configuration:}")
+    private String perTenantLimitsConfiguration;
+    @Value("${server.rest.limits.customer.enabled:false}")
+    private boolean perCustomerLimitsEnabled;
+    @Value("${server.rest.limits.customer.configuration:}")
+    private String perCustomerLimitsConfiguration;
+
+    @Autowired
+    private ThingsboardErrorResponseHandler errorResponseHandler;
+
+    private ConcurrentMap<TenantId, TbRateLimits> perTenantLimits = new ConcurrentHashMap<>();
+    private ConcurrentMap<CustomerId, TbRateLimits> perCustomerLimits = new ConcurrentHashMap<>();
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        SecurityUser user = getCurrentUser();
+        if (user != null && !user.isSystemAdmin()) {
+            if (perTenantLimitsEnabled) {
+                TbRateLimits rateLimits = perTenantLimits.computeIfAbsent(user.getTenantId(), id -> new TbRateLimits(perTenantLimitsConfiguration));
+                if (!rateLimits.tryConsume()) {
+                    errorResponseHandler.handle(new TbRateLimitsException(EntityType.TENANT), (HttpServletResponse) response);
+                    return;
+                }
+            }
+            if (perCustomerLimitsEnabled && user.isCustomerUser()) {
+                TbRateLimits rateLimits = perCustomerLimits.computeIfAbsent(user.getCustomerId(), id -> new TbRateLimits(perCustomerLimitsConfiguration));
+                if (!rateLimits.tryConsume()) {
+                    errorResponseHandler.handle(new TbRateLimitsException(EntityType.CUSTOMER), (HttpServletResponse) response);
+                    return;
+                }
+            }
+        }
+        chain.doFilter(request, response);
+    }
+
+    protected SecurityUser getCurrentUser() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) {
+            return (SecurityUser) authentication.getPrincipal();
+        } else {
+            return null;
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 6afa6b2..1901e49 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -91,6 +91,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
 
     @Autowired private ObjectMapper objectMapper;
 
+    @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter;
+
     @Bean
     protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
         RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
@@ -186,7 +188,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
                 .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
                 .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
                 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
-                .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
+                .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
+                .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class);
     }
 
 
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 9790a2d..187103a 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -52,6 +52,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
 import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
 import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
 import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.attributes.AttributesService;
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
index 136bbfd..ebd068d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
@@ -18,14 +18,21 @@ package org.thingsboard.server.controller.plugin;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.BeanCreationNotAllowedException;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Lazy;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
 import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.TextMessage;
 import org.springframework.web.socket.WebSocketSession;
 import org.springframework.web.socket.handler.TextWebSocketHandler;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.msg.tools.TbRateLimits;
 import org.thingsboard.server.config.WebSocketConfiguration;
 import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
 import org.thingsboard.server.service.telemetry.SessionEvent;
 import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint;
 import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
@@ -34,6 +41,7 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
 import java.io.IOException;
 import java.net.URI;
 import java.security.InvalidParameterException;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -48,12 +56,32 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
     @Autowired
     private TelemetryWebSocketService webSocketService;
 
+    @Value("${server.ws.limits.max_sessions_per_tenant:0}")
+    private int maxSessionsPerTenant;
+    @Value("${server.ws.limits.max_sessions_per_customer:0}")
+    private int maxSessionsPerCustomer;
+    @Value("${server.ws.limits.max_sessions_per_regular_user:0}")
+    private int maxSessionsPerRegularUser;
+    @Value("${server.ws.limits.max_sessions_per_public_user:0}")
+    private int maxSessionsPerPublicUser;
+
+    @Value("${server.ws.limits.max_updates_per_session:}")
+    private String perSessionUpdatesConfiguration;
+
+    private ConcurrentMap<String, TelemetryWebSocketSessionRef> blacklistedSessions = new ConcurrentHashMap<>();
+    private ConcurrentMap<String, TbRateLimits> perSessionUpdateLimits = new ConcurrentHashMap<>();
+
+    private ConcurrentMap<TenantId, Set<String>> tenantSessionsMap = new ConcurrentHashMap<>();
+    private ConcurrentMap<CustomerId, Set<String>> customerSessionsMap = new ConcurrentHashMap<>();
+    private ConcurrentMap<UserId, Set<String>> regularUserSessionsMap = new ConcurrentHashMap<>();
+    private ConcurrentMap<UserId, Set<String>> publicUserSessionsMap = new ConcurrentHashMap<>();
+
     @Override
     public void handleTextMessage(WebSocketSession session, TextMessage message) {
         try {
-            log.info("[{}] Processing {}", session.getId(), message);
             SessionMetaData sessionMd = internalSessionMap.get(session.getId());
             if (sessionMd != null) {
+                log.info("[{}][{}] Processing {}", sessionMd.sessionRef.getSecurityCtx().getTenantId(), session.getId(), message.getPayload());
                 webSocketService.handleWebSocketMsg(sessionMd.sessionRef, message.getPayload());
             } else {
                 log.warn("[{}] Failed to find session", session.getId());
@@ -71,12 +99,15 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
             String internalSessionId = session.getId();
             TelemetryWebSocketSessionRef sessionRef = toRef(session);
             String externalSessionId = sessionRef.getSessionId();
+            if (!checkLimits(session, sessionRef)) {
+                return;
+            }
             internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef));
             externalSessionMap.put(externalSessionId, internalSessionId);
             processInWebSocketService(sessionRef, SessionEvent.onEstablished());
-            log.info("[{}][{}] Session is started", externalSessionId, session.getId());
+            log.info("[{}][{}][{}] Session is opened", sessionRef.getSecurityCtx().getTenantId(), externalSessionId, session.getId());
         } catch (InvalidParameterException e) {
-            log.warn("[[{}] Failed to start session", session.getId(), e);
+            log.warn("[{}] Failed to start session", session.getId(), e);
             session.close(CloseStatus.BAD_DATA.withReason(e.getMessage()));
         } catch (Exception e) {
             log.warn("[{}] Failed to start session", session.getId(), e);
@@ -101,6 +132,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
         super.afterConnectionClosed(session, closeStatus);
         SessionMetaData sessionMd = internalSessionMap.remove(session.getId());
         if (sessionMd != null) {
+            cleanupLimits(session, sessionMd.sessionRef);
             externalSessionMap.remove(sessionMd.sessionRef.getSessionId());
             processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed());
         }
@@ -136,7 +168,7 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
         private final WebSocketSession session;
         private final TelemetryWebSocketSessionRef sessionRef;
 
-        public SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) {
+        SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) {
             super();
             this.session = session;
             this.sessionRef = sessionRef;
@@ -144,13 +176,29 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
     }
 
     @Override
-    public void send(TelemetryWebSocketSessionRef sessionRef, String msg) throws IOException {
+    public void send(TelemetryWebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException {
         String externalId = sessionRef.getSessionId();
         log.debug("[{}] Processing {}", externalId, msg);
         String internalId = externalSessionMap.get(externalId);
         if (internalId != null) {
             SessionMetaData sessionMd = internalSessionMap.get(internalId);
             if (sessionMd != null) {
+                if (!StringUtils.isEmpty(perSessionUpdatesConfiguration)) {
+                    TbRateLimits rateLimits = perSessionUpdateLimits.computeIfAbsent(sessionRef.getSessionId(), sid -> new TbRateLimits(perSessionUpdatesConfiguration));
+                    if (!rateLimits.tryConsume()) {
+                        if (blacklistedSessions.putIfAbsent(externalId, sessionRef) == null) {
+                            log.info("[{}][{}][{}] Failed to process session update. Max session updates limit reached"
+                                    , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId);
+                            synchronized (sessionMd) {
+                                sessionMd.session.sendMessage(new TextMessage("{\"subscriptionId\":" + subscriptionId + ", \"errorCode\":" + ThingsboardErrorCode.TOO_MANY_UPDATES.getErrorCode() + ", \"errorMsg\":\"Too many updates!\"}"));
+                            }
+                        }
+                        return;
+                    } else {
+                        log.debug("[{}][{}][{}] Session is no longer blacklisted.", sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), externalId);
+                        blacklistedSessions.remove(externalId);
+                    }
+                }
                 synchronized (sessionMd) {
                     sessionMd.session.sendMessage(new TextMessage(msg));
                 }
@@ -163,14 +211,14 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
     }
 
     @Override
-    public void close(TelemetryWebSocketSessionRef sessionRef) throws IOException {
+    public void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus reason) throws IOException {
         String externalId = sessionRef.getSessionId();
         log.debug("[{}] Processing close request", externalId);
         String internalId = externalSessionMap.get(externalId);
         if (internalId != null) {
             SessionMetaData sessionMd = internalSessionMap.get(internalId);
             if (sessionMd != null) {
-                sessionMd.session.close(CloseStatus.NORMAL);
+                sessionMd.session.close(reason);
             } else {
                 log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId);
             }
@@ -179,4 +227,96 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr
         }
     }
 
+    private boolean checkLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) throws Exception {
+        String sessionId = session.getId();
+        if (maxSessionsPerTenant > 0) {
+            Set<String> tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
+            synchronized (tenantSessions) {
+                if (tenantSessions.size() < maxSessionsPerTenant) {
+                    tenantSessions.add(sessionId);
+                } else {
+                    log.info("[{}][{}][{}] Failed to start session. Max tenant sessions limit reached"
+                            , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
+                    session.close(CloseStatus.POLICY_VIOLATION.withReason("Max tenant sessions limit reached!"));
+                    return false;
+                }
+            }
+        }
+
+        if (sessionRef.getSecurityCtx().isCustomerUser()) {
+            if (maxSessionsPerCustomer > 0) {
+                Set<String> customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (customerSessions) {
+                    if (customerSessions.size() < maxSessionsPerCustomer) {
+                        customerSessions.add(sessionId);
+                    } else {
+                        log.info("[{}][{}][{}] Failed to start session. Max customer sessions limit reached"
+                                , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
+                        session.close(CloseStatus.POLICY_VIOLATION.withReason("Max customer sessions limit reached"));
+                        return false;
+                    }
+                }
+            }
+            if (maxSessionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                Set<String> regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (regularUserSessions) {
+                    if (regularUserSessions.size() < maxSessionsPerRegularUser) {
+                        regularUserSessions.add(sessionId);
+                    } else {
+                        log.info("[{}][{}][{}] Failed to start session. Max regular user sessions limit reached"
+                                , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
+                        session.close(CloseStatus.POLICY_VIOLATION.withReason("Max regular user sessions limit reached"));
+                        return false;
+                    }
+                }
+            }
+            if (maxSessionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (publicUserSessions) {
+                    if (publicUserSessions.size() < maxSessionsPerPublicUser) {
+                        publicUserSessions.add(sessionId);
+                    } else {
+                        log.info("[{}][{}][{}] Failed to start session. Max public user sessions limit reached"
+                                , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), sessionId);
+                        session.close(CloseStatus.POLICY_VIOLATION.withReason("Max public user sessions limit reached"));
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    private void cleanupLimits(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) {
+        String sessionId = session.getId();
+        perSessionUpdateLimits.remove(sessionRef.getSessionId());
+        blacklistedSessions.remove(sessionRef.getSessionId());
+        if (maxSessionsPerTenant > 0) {
+            Set<String> tenantSessions = tenantSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
+            synchronized (tenantSessions) {
+                tenantSessions.remove(sessionId);
+            }
+        }
+        if (sessionRef.getSecurityCtx().isCustomerUser()) {
+            if (maxSessionsPerCustomer > 0) {
+                Set<String> customerSessions = customerSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (customerSessions) {
+                    customerSessions.remove(sessionId);
+                }
+            }
+            if (maxSessionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                Set<String> regularUserSessions = regularUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (regularUserSessions) {
+                    regularUserSessions.remove(sessionId);
+                }
+            }
+            if (maxSessionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                Set<String> publicUserSessions = publicUserSessionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (publicUserSessions) {
+                    publicUserSessions.remove(sessionId);
+                }
+            }
+        }
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
index 63fe17a..f906234 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
@@ -25,8 +25,10 @@ import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
 import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
 
@@ -34,6 +36,7 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+
 @Component
 @Slf4j
 public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
@@ -62,6 +65,8 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
 
                 if (exception instanceof ThingsboardException) {
                     handleThingsboardException((ThingsboardException) exception, response);
+                } else if (exception instanceof TbRateLimitsException) {
+                    handleRateLimitException(response, (TbRateLimitsException) exception);
                 } else if (exception instanceof AccessDeniedException) {
                     handleAccessDeniedException(response);
                 } else if (exception instanceof AuthenticationException) {
@@ -77,6 +82,7 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
         }
     }
 
+
     private void handleThingsboardException(ThingsboardException thingsboardException, HttpServletResponse response) throws IOException {
 
         ThingsboardErrorCode errorCode = thingsboardException.getErrorCode();
@@ -110,6 +116,15 @@ public class ThingsboardErrorResponseHandler implements AccessDeniedHandler {
         mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(thingsboardException.getMessage(), errorCode, status));
     }
 
+    private void handleRateLimitException(HttpServletResponse response, TbRateLimitsException exception) throws IOException {
+        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
+        String message = "Too many requests for current " + exception.getEntityType().name().toLowerCase() + "!";
+        mapper.writeValue(response.getWriter(),
+                ThingsboardErrorResponse.of(message,
+                        ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS));
+    }
+
+
     private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
         response.setStatus(HttpStatus.FORBIDDEN.value());
         mapper.writeValue(response.getWriter(),
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
index 1d95b9e..683a8c5 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
@@ -23,12 +23,17 @@ 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.Service;
 import org.springframework.util.StringUtils;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketSession;
 import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.kv.Aggregation;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
@@ -42,6 +47,7 @@ import org.thingsboard.server.service.security.AccessValidator;
 import org.thingsboard.server.service.security.ValidationCallback;
 import org.thingsboard.server.service.security.ValidationResult;
 import org.thingsboard.server.service.security.ValidationResultCode;
+import org.thingsboard.server.service.security.model.UserPrincipal;
 import org.thingsboard.server.service.telemetry.cmd.AttributesSubscriptionCmd;
 import org.thingsboard.server.service.telemetry.cmd.GetHistoryCmd;
 import org.thingsboard.server.service.telemetry.cmd.SubscriptionCmd;
@@ -64,6 +70,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -72,6 +79,10 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -82,8 +93,8 @@ import java.util.stream.Collectors;
 @Slf4j
 public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService {
 
-    public static final int DEFAULT_LIMIT = 100;
-    public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
+    private static final int DEFAULT_LIMIT = 100;
+    private static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
     private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
     private static final String PROCESSING_MSG = "[{}] Processing: {}";
     private static final ObjectMapper jsonMapper = new ObjectMapper();
@@ -108,11 +119,25 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
     @Autowired
     private TimeseriesService tsService;
 
+    @Value("${server.ws.limits.max_subscriptions_per_tenant:0}")
+    private int maxSubscriptionsPerTenant;
+    @Value("${server.ws.limits.max_subscriptions_per_customer:0}")
+    private int maxSubscriptionsPerCustomer;
+    @Value("${server.ws.limits.max_subscriptions_per_regular_user:0}")
+    private int maxSubscriptionsPerRegularUser;
+    @Value("${server.ws.limits.max_subscriptions_per_public_user:0}")
+    private int maxSubscriptionsPerPublicUser;
+
+    private ConcurrentMap<TenantId, Set<String>> tenantSubscriptionsMap = new ConcurrentHashMap<>();
+    private ConcurrentMap<CustomerId, Set<String>> customerSubscriptionsMap = new ConcurrentHashMap<>();
+    private ConcurrentMap<UserId, Set<String>> regularUserSubscriptionsMap = new ConcurrentHashMap<>();
+    private ConcurrentMap<UserId, Set<String>> publicUserSubscriptionsMap = new ConcurrentHashMap<>();
+
     private ExecutorService executor;
 
     @PostConstruct
     public void initExecutor() {
-        executor = Executors.newSingleThreadExecutor();
+        executor = new ThreadPoolExecutor(0, 50, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
     }
 
     @PreDestroy
@@ -136,6 +161,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
             case CLOSED:
                 wsSessionsMap.remove(sessionId);
                 subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+                processSessionClose(sessionRef);
                 break;
         }
     }
@@ -150,10 +176,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
             TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class);
             if (cmdsWrapper != null) {
                 if (cmdsWrapper.getAttrSubCmds() != null) {
-                    cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(sessionRef, cmd));
+                    cmdsWrapper.getAttrSubCmds().forEach(cmd -> {
+                        if (processSubscription(sessionRef, cmd)) {
+                            handleWsAttributesSubscriptionCmd(sessionRef, cmd);
+                        }
+                    });
                 }
                 if (cmdsWrapper.getTsSubCmds() != null) {
-                    cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(sessionRef, cmd));
+                    cmdsWrapper.getTsSubCmds().forEach(cmd -> {
+                        if (processSubscription(sessionRef, cmd)) {
+                            handleWsTimeseriesSubscriptionCmd(sessionRef, cmd);
+                        }
+                    });
                 }
                 if (cmdsWrapper.getHistoryCmds() != null) {
                     cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
@@ -174,6 +208,105 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
         }
     }
 
+    private void processSessionClose(TelemetryWebSocketSessionRef sessionRef) {
+        String sessionId = "[" + sessionRef.getSessionId() + "]";
+        if (maxSubscriptionsPerTenant > 0) {
+            Set<String> tenantSubscriptions = tenantSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
+            synchronized (tenantSubscriptions) {
+                tenantSubscriptions.removeIf(subId -> subId.startsWith(sessionId));
+            }
+        }
+        if (sessionRef.getSecurityCtx().isCustomerUser()) {
+            if (maxSubscriptionsPerCustomer > 0) {
+                Set<String> customerSessions = customerSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (customerSessions) {
+                    customerSessions.removeIf(subId -> subId.startsWith(sessionId));
+                }
+            }
+            if (maxSubscriptionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                Set<String> regularUserSessions = regularUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (regularUserSessions) {
+                    regularUserSessions.removeIf(subId -> subId.startsWith(sessionId));
+                }
+            }
+            if (maxSubscriptionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                Set<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (publicUserSessions) {
+                    publicUserSessions.removeIf(subId -> subId.startsWith(sessionId));
+                }
+            }
+        }
+    }
+
+    private boolean processSubscription(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+        String subId = "[" + sessionRef.getSessionId() + "]:[" + cmd.getCmdId() + "]";
+        try {
+            if (maxSubscriptionsPerTenant > 0) {
+                Set<String> tenantSubscriptions = tenantSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getTenantId(), id -> ConcurrentHashMap.newKeySet());
+                synchronized (tenantSubscriptions) {
+                    if (cmd.isUnsubscribe()) {
+                        tenantSubscriptions.remove(subId);
+                    } else if (tenantSubscriptions.size() < maxSubscriptionsPerTenant) {
+                        tenantSubscriptions.add(subId);
+                    } else {
+                        log.info("[{}][{}][{}] Failed to start subscription. Max tenant subscriptions limit reached"
+                                , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId);
+                        msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max tenant subscriptions limit reached!"));
+                        return false;
+                    }
+                }
+            }
+
+            if (sessionRef.getSecurityCtx().isCustomerUser()) {
+                if (maxSubscriptionsPerCustomer > 0) {
+                    Set<String> customerSessions = customerSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getCustomerId(), id -> ConcurrentHashMap.newKeySet());
+                    synchronized (customerSessions) {
+                        if (cmd.isUnsubscribe()) {
+                            customerSessions.remove(subId);
+                        } else if (customerSessions.size() < maxSubscriptionsPerCustomer) {
+                            customerSessions.add(subId);
+                        } else {
+                            log.info("[{}][{}][{}] Failed to start subscription. Max customer subscriptions limit reached"
+                                    , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId);
+                            msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max customer subscriptions limit reached"));
+                            return false;
+                        }
+                    }
+                }
+                if (maxSubscriptionsPerRegularUser > 0 && UserPrincipal.Type.USER_NAME.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                    Set<String> regularUserSessions = regularUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                    synchronized (regularUserSessions) {
+                        if (regularUserSessions.size() < maxSubscriptionsPerRegularUser) {
+                            regularUserSessions.add(subId);
+                        } else {
+                            log.info("[{}][{}][{}] Failed to start subscription. Max regular user subscriptions limit reached"
+                                    , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId);
+                            msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max regular user subscriptions limit reached"));
+                            return false;
+                        }
+                    }
+                }
+                if (maxSubscriptionsPerPublicUser > 0 && UserPrincipal.Type.PUBLIC_ID.equals(sessionRef.getSecurityCtx().getUserPrincipal().getType())) {
+                    Set<String> publicUserSessions = publicUserSubscriptionsMap.computeIfAbsent(sessionRef.getSecurityCtx().getId(), id -> ConcurrentHashMap.newKeySet());
+                    synchronized (publicUserSessions) {
+                        if (publicUserSessions.size() < maxSubscriptionsPerPublicUser) {
+                            publicUserSessions.add(subId);
+                        } else {
+                            log.info("[{}][{}][{}] Failed to start subscription. Max public user subscriptions limit reached"
+                                    , sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), subId);
+                            msgEndpoint.close(sessionRef, CloseStatus.POLICY_VIOLATION.withReason("Max public user subscriptions limit reached"));
+                            return false;
+                        }
+                    }
+                }
+            }
+        } catch (IOException e) {
+            log.warn("[{}] Failed to send session close: {}", sessionRef.getSessionId(), e);
+            return false;
+        }
+        return true;
+    }
+
     private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
         String sessionId = sessionRef.getSessionId();
         log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -216,7 +349,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
             public void onFailure(Throwable e) {
                 log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
                 SubscriptionUpdate update;
-                if (UnauthorizedException.class.isInstance(e)) {
+                if (e instanceof UnauthorizedException) {
                     update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
                             SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
                 } else {
@@ -449,7 +582,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
 
     private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) {
         try {
-            msgEndpoint.send(sessionRef, jsonMapper.writeValueAsString(update));
+            msgEndpoint.send(sessionRef, update.getSubscriptionId(), jsonMapper.writeValueAsString(update));
         } catch (JsonProcessingException e) {
             log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
         } catch (IOException e) {
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java
index 00fb80a..b73aadf 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketMsgEndpoint.java
@@ -15,6 +15,8 @@
  */
 package org.thingsboard.server.service.telemetry;
 
+import org.springframework.web.socket.CloseStatus;
+
 import java.io.IOException;
 
 /**
@@ -22,8 +24,7 @@ import java.io.IOException;
  */
 public interface TelemetryWebSocketMsgEndpoint {
 
-    void send(TelemetryWebSocketSessionRef sessionRef, String msg) throws IOException;
-
-    void close(TelemetryWebSocketSessionRef sessionRef) throws IOException;
+    void send(TelemetryWebSocketSessionRef sessionRef, int subscriptionId, String msg) throws IOException;
 
+    void close(TelemetryWebSocketSessionRef sessionRef, CloseStatus withReason) throws IOException;
 }
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 45f8433..4b4fec3 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
@@ -112,7 +112,6 @@ public class RemoteRuleEngineTransportService implements RuleEngineTransportServ
     public void init() {
         TBKafkaProducerTemplate.TBKafkaProducerTemplateBuilder<ToTransportMsg> notificationsProducerBuilder = TBKafkaProducerTemplate.builder();
         notificationsProducerBuilder.settings(kafkaSettings);
-        notificationsProducerBuilder.defaultTopic(notificationsTopic);
         notificationsProducerBuilder.encoder(new ToTransportMsgEncoder());
 
         notificationsProducer = notificationsProducerBuilder.build();
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/RemoteTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/RemoteTransportApiService.java
index 88033aa..2ced33a 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/RemoteTransportApiService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/RemoteTransportApiService.java
@@ -29,6 +29,7 @@ import org.thingsboard.server.kafka.*;
 import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -43,8 +44,6 @@ public class RemoteTransportApiService {
 
     @Value("${transport.remote.transport_api.requests_topic}")
     private String transportApiRequestsTopic;
-    @Value("${transport.remote.transport_api.responses_topic}")
-    private String transportApiResponsesTopic;
     @Value("${transport.remote.transport_api.max_pending_requests}")
     private int maxPendingRequests;
     @Value("${transport.remote.transport_api.request_timeout}")
@@ -69,11 +68,10 @@ public class RemoteTransportApiService {
 
     @PostConstruct
     public void init() {
-        this.transportCallbackExecutor = new ThreadPoolExecutor(0, 100, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
+        this.transportCallbackExecutor = new ThreadPoolExecutor(0, 100, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
 
         TBKafkaProducerTemplate.TBKafkaProducerTemplateBuilder<TransportApiResponseMsg> responseBuilder = TBKafkaProducerTemplate.builder();
         responseBuilder.settings(kafkaSettings);
-        responseBuilder.defaultTopic(transportApiResponsesTopic);
         responseBuilder.encoder(new TransportApiResponseEncoder());
 
         TBKafkaConsumerTemplate.TBKafkaConsumerTemplateBuilder<TransportApiRequestMsg> requestBuilder = TBKafkaConsumerTemplate.builder();
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 52f6395..aee49f6 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -32,6 +32,26 @@ server:
     # Alias that identifies the key in the key store
     key-alias: "${SSL_KEY_ALIAS:tomcat}"
   log_controller_error_stack_trace: "${HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE:true}"
+  ws:
+    limits:
+      # Limit the amount of sessions and subscriptions available on each server. Put values to zero to disable particular limitation
+      max_sessions_per_tenant: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_TENANT:0}"
+      max_sessions_per_customer: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_CUSTOMER:0}"
+      max_sessions_per_regular_user: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_REGULAR_USER:0}"
+      max_sessions_per_public_user: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_PUBLIC_USER:0}"
+      max_subscriptions_per_tenant: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_TENANT:0}"
+      max_subscriptions_per_customer: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_CUSTOMER:0}"
+      max_subscriptions_per_regular_user: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_REGULAR_USER:0}"
+      max_subscriptions_per_public_user: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SUBSCRIPTIONS_PER_PUBLIC_USER:0}"
+      max_updates_per_session: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_UPDATES_PER_SESSION:300:1,3000:60}"
+  rest:
+    limits:
+      tenant:
+        enabled: "${TB_SERVER_REST_LIMITS_TENANT_ENABLED:false}"
+        configuration: "${TB_SERVER_REST_LIMITS_TENANT_CONFIGURATION:100:1,2000:60}"
+      customer:
+        enabled: "${TB_SERVER_REST_LIMITS_CUSTOMER_ENABLED:false}"
+        configuration: "${TB_SERVER_REST_LIMITS_CUSTOMER_CONFIGURATION:50:1,1000:60}"
 
 # Zookeeper connection parameters. Used for service discovery.
 zk:
@@ -55,6 +75,8 @@ rpc:
 
 # Clustering properties related to consistent-hashing. See architecture docs for more details.
 cluster:
+  # Unique id for this node (autogenerated if empty)
+  node_id: "${CLUSTER_NODE_ID:}"
   # Name of hash function used for consistent hash ring.
   hash_function_name: "${CLUSTER_HASH_FUNCTION_NAME:murmur3_128}"
   # Amount of virtual nodes in consistent hash ring.
@@ -392,7 +414,6 @@ transport:
   remote:
     transport_api:
       requests_topic: "${TB_TRANSPORT_API_REQUEST_TOPIC:tb.transport.api.requests}"
-      responses_topic: "${TB_TRANSPORT_API_RESPONSE_TOPIC:tb.transport.api.responses}"
       max_pending_requests: "${TB_TRANSPORT_MAX_PENDING_REQUESTS:10000}"
       request_timeout: "${TB_TRANSPORT_MAX_REQUEST_TIMEOUT:10000}"
       request_poll_interval: "${TB_TRANSPORT_RESPONSE_POLL_INTERVAL_MS:25}"
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
index 5b7cfb9..3f7cde3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
@@ -25,7 +25,9 @@ public enum ThingsboardErrorCode {
     PERMISSION_DENIED(20),
     INVALID_ARGUMENTS(30),
     BAD_REQUEST_PARAMS(31),
-    ITEM_NOT_FOUND(32);
+    ITEM_NOT_FOUND(32),
+    TOO_MANY_REQUESTS(33),
+    TOO_MANY_UPDATES(34);
 
     private int errorCode;
 
diff --git a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaAdmin.java
index f6bafef..7cc90a0 100644
--- a/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaAdmin.java
+++ b/common/queue/src/main/java/org/thingsboard/server/kafka/TBKafkaAdmin.java
@@ -15,15 +15,17 @@
  */
 package org.thingsboard.server.kafka;
 
-import org.apache.kafka.clients.admin.AdminClient;
-import org.apache.kafka.clients.admin.CreateTopicsResult;
-import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.admin.*;
 import org.apache.kafka.clients.consumer.ConsumerRecords;
 import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.KafkaFuture;
 
 import java.time.Duration;
 import java.util.Collections;
 import java.util.Properties;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Created by ashvayka on 24.09.18.
@@ -36,7 +38,32 @@ public class TBKafkaAdmin {
         client = AdminClient.create(settings.toProps());
     }
 
+    public void waitForTopic(String topic, long timeout, TimeUnit timeoutUnit) throws InterruptedException, TimeoutException {
+        synchronized (this) {
+            long timeoutExpiredMs = System.currentTimeMillis() + timeoutUnit.toMillis(timeout);
+            while (!topicExists(topic)) {
+                long waitMs = timeoutExpiredMs - System.currentTimeMillis();
+                if (waitMs <= 0) {
+                    throw new TimeoutException("Timeout occurred while waiting for topic [" + topic + "] to be available!");
+                } else {
+                    wait(1000);
+                }
+            }
+        }
+    }
+
     public CreateTopicsResult createTopic(NewTopic topic){
         return client.createTopics(Collections.singletonList(topic));
     }
+
+    private boolean topicExists(String topic) throws InterruptedException {
+        KafkaFuture<TopicDescription> topicDescriptionFuture = client.describeTopics(Collections.singleton(topic)).values().get(topic);
+        try {
+            topicDescriptionFuture.get();
+            return true;
+        } catch (ExecutionException e) {
+            return false;
+        }
+    }
+
 }
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 ee652f4..6cf507f 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
@@ -20,14 +20,17 @@ import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.kafka.clients.admin.CreateTopicsResult;
 import org.apache.kafka.clients.admin.NewTopic;
+import org.apache.kafka.clients.admin.TopicDescription;
 import org.apache.kafka.clients.producer.Callback;
 import org.apache.kafka.clients.producer.KafkaProducer;
 import org.apache.kafka.clients.producer.ProducerConfig;
 import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.clients.producer.RecordMetadata;
+import org.apache.kafka.common.KafkaFuture;
 import org.apache.kafka.common.PartitionInfo;
 import org.apache.kafka.common.errors.TopicExistsException;
 import org.apache.kafka.common.header.Header;
+import org.springframework.util.StringUtils;
 
 import java.util.List;
 import java.util.Properties;
@@ -35,6 +38,7 @@ import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Created by ashvayka on 24.09.18.
@@ -71,21 +75,19 @@ public class TBKafkaProducerTemplate<T> {
     }
 
     public void init() {
-        try {
-            TBKafkaAdmin admin = new TBKafkaAdmin(this.settings);
-            CreateTopicsResult result = admin.createTopic(new NewTopic(defaultTopic, 100, (short) 1));
-            result.all().get();
-        } catch (Exception e) {
-            if ((e instanceof TopicExistsException) || (e.getCause() != null && e.getCause() instanceof TopicExistsException)) {
-                log.trace("[{}] Topic already exists.", defaultTopic);
-            } else {
-                log.info("[{}] Failed to create topic: {}", defaultTopic, e.getMessage(), e);
+        this.partitionInfoMap = new ConcurrentHashMap<>();
+        if (!StringUtils.isEmpty(defaultTopic)) {
+            try {
+                TBKafkaAdmin admin = new TBKafkaAdmin(this.settings);
+                admin.waitForTopic(defaultTopic, 30, TimeUnit.SECONDS);
+                log.info("[{}] Topic exists.", defaultTopic);
+            } catch (Exception e) {
+                log.info("[{}] Failed to wait for topic: {}", defaultTopic, e.getMessage(), e);
                 throw new RuntimeException(e);
             }
+            //Maybe this should not be cached, but we don't plan to change size of partitions
+            this.partitionInfoMap.putIfAbsent(defaultTopic, producer.partitionsFor(defaultTopic));
         }
-        //Maybe this should not be cached, but we don't plan to change size of partitions
-        this.partitionInfoMap = new ConcurrentHashMap<>();
-        this.partitionInfoMap.putIfAbsent(defaultTopic, producer.partitionsFor(defaultTopic));
     }
 
     T enrich(T value, String responseTopic, UUID requestId) {
@@ -105,7 +107,11 @@ public class TBKafkaProducerTemplate<T> {
     }
 
     public Future<RecordMetadata> send(String key, T value, Long timestamp, Iterable<Header> headers, Callback callback) {
-        return send(this.defaultTopic, key, value, timestamp, headers, callback);
+        if (!StringUtils.isEmpty(this.defaultTopic)) {
+            return send(this.defaultTopic, key, value, timestamp, headers, callback);
+        } else {
+            throw new RuntimeException("Failed to send message! Default topic is not specified!");
+        }
     }
 
     public Future<RecordMetadata> send(String topic, String key, T value, Iterable<Header> headers, Callback callback) {
diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
index c9f681f..9385844 100644
--- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
+++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/AbstractTransportService.java
@@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.msg.tools.TbRateLimits;
+import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
 import org.thingsboard.server.common.transport.SessionMsgListener;
 import org.thingsboard.server.common.transport.TransportService;
 import org.thingsboard.server.common.transport.TransportServiceCallback;
@@ -31,6 +32,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -276,7 +278,7 @@ public abstract class AbstractTransportService implements TransportService {
             new TbRateLimits(perDevicesLimitsConf);
         }
         this.schedulerExecutor = Executors.newSingleThreadScheduledExecutor();
-        this.transportCallbackExecutor = new ThreadPoolExecutor(0, 20, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
+        this.transportCallbackExecutor = new ThreadPoolExecutor(0, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
         this.schedulerExecutor.scheduleAtFixedRate(this::checkInactivityAndReportActivity, sessionReportTimeout, sessionReportTimeout, TimeUnit.MILLISECONDS);
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index 096484f..b32232e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -214,7 +214,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
         if (customer == null) {
             throw new DataValidationException("Can't unassign dashboards from non-existent customer!");
         }
-        new CustomerDashboardsUnassigner(customer).removeEntities(customer);
+        new CustomerDashboardsUnassigner(customer).removeEntities(tenantId, customer);
     }
 
     @Override
@@ -225,7 +225,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
         if (customer == null) {
             throw new DataValidationException("Can't update dashboards for non-existent customer!");
         }
-        new CustomerDashboardsUpdater(customer).removeEntities(customer);
+        new CustomerDashboardsUpdater(customer).removeEntities(tenantId, customer);
     }
 
     private DataValidator<Dashboard> dashboardValidator =
@@ -269,7 +269,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
         }
 
         @Override
-        protected List<DashboardInfo> findEntities(Customer customer, TimePageLink pageLink) {
+        protected List<DashboardInfo> findEntities(TenantId tenantId, Customer customer, TimePageLink pageLink) {
             try {
                 return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get();
             } catch (InterruptedException | ExecutionException e) {
@@ -279,7 +279,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
         }
 
         @Override
-        protected void removeEntity(DashboardInfo entity) {
+        protected void removeEntity(TenantId tenantId, DashboardInfo entity) {
             unassignDashboardFromCustomer(customer.getTenantId(), new DashboardId(entity.getUuidId()), this.customer.getId());
         }
         
@@ -294,7 +294,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
         }
 
         @Override
-        protected List<DashboardInfo> findEntities(Customer customer, TimePageLink pageLink) {
+        protected List<DashboardInfo> findEntities(TenantId tenantId, Customer customer, TimePageLink pageLink) {
             try {
                 return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get();
             } catch (InterruptedException | ExecutionException e) {
@@ -304,7 +304,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
         }
 
         @Override
-        protected void removeEntity(DashboardInfo entity) {
+        protected void removeEntity(TenantId tenantId, DashboardInfo entity) {
             updateAssignedCustomer(customer.getTenantId(), new DashboardId(entity.getUuidId()), this.customer);
         }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java
index cda4217..5c9f717 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EntityViewEntity.java
@@ -60,10 +60,6 @@ public class EntityViewEntity implements SearchTextEntity<EntityView> {
     @Column(name = ID_PROPERTY)
     private UUID id;
 
-    @Enumerated(EnumType.STRING)
-    @Column(name = ENTITY_TYPE_PROPERTY)
-    private EntityType entityType;
-
     @PartitionKey(value = 1)
     @Column(name = ModelConstants.ENTITY_VIEW_TENANT_ID_PROPERTY)
     private UUID tenantId;
@@ -76,6 +72,10 @@ public class EntityViewEntity implements SearchTextEntity<EntityView> {
     @Column(name = DEVICE_TYPE_PROPERTY)
     private String type;
 
+    @Enumerated(EnumType.STRING)
+    @Column(name = ENTITY_TYPE_PROPERTY)
+    private EntityType entityType;
+
     @Column(name = ModelConstants.ENTITY_VIEW_ENTITY_ID_PROPERTY)
     private UUID entityId;
 
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 be40bf4..7de20d9 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
@@ -17,7 +17,6 @@ package org.thingsboard.server.dao.nosql;
 
 import com.datastax.driver.core.ResultSet;
 import com.datastax.driver.core.ResultSetFuture;
-import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.SettableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -26,7 +25,6 @@ import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.dao.entity.EntityService;
-import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor;
 import org.thingsboard.server.dao.util.AsyncTaskContext;
 import org.thingsboard.server.dao.util.NoSqlAnyDao;
@@ -34,7 +32,6 @@ import org.thingsboard.server.dao.util.NoSqlAnyDao;
 import javax.annotation.PreDestroy;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.ExecutionException;
 
 /**
  * Created by ashvayka on 24.10.18.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
index 52fcf85..c223b76 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.dao.service;
 
 import org.thingsboard.server.common.data.id.IdBased;
+import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageLink;
 
 import java.util.List;
@@ -25,13 +26,13 @@ public abstract class TimePaginatedRemover<I, D extends IdBased<?>> {
 
     private static final int DEFAULT_LIMIT = 100;
 
-    public void removeEntities(I id) {
+    public void removeEntities(TenantId tenantId, I id) {
         TimePageLink pageLink = new TimePageLink(DEFAULT_LIMIT);
         boolean hasNext = true;
         while (hasNext) {
-            List<D> entities = findEntities(id, pageLink);
+            List<D> entities = findEntities(tenantId, id, pageLink);
             for (D entity : entities) {
-                removeEntity(entity);
+                removeEntity(tenantId, entity);
             }
             hasNext = entities.size() == pageLink.getLimit();
             if (hasNext) {
@@ -42,8 +43,8 @@ public abstract class TimePaginatedRemover<I, D extends IdBased<?>> {
         }
     }
 
-    protected abstract List<D> findEntities(I id, TimePageLink pageLink);
+    protected abstract List<D> findEntities(TenantId tenantId, I id, TimePageLink pageLink);
 
-    protected abstract void removeEntity(D entity);
+    protected abstract void removeEntity(TenantId tenantId, D entity);
 
 }
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 65848fc..b204a44 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
@@ -20,12 +20,10 @@ import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.SettableFuture;
 import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.msg.tools.TbRateLimits;
 
 import javax.annotation.Nullable;
-import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ConcurrentHashMap;
@@ -33,11 +31,12 @@ import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
 
 /**
  * Created by ashvayka on 24.10.18.
@@ -73,7 +72,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
         this.concurrencyLimit = concurrencyLimit;
         this.queue = new LinkedBlockingDeque<>(queueLimit);
         this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads);
-        this.callbackExecutor = Executors.newFixedThreadPool(callbackThreads);
+        this.callbackExecutor = new ThreadPoolExecutor(callbackThreads, 50, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
         this.timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
         this.perTenantLimitsEnabled = perTenantLimitsEnabled;
         this.perTenantLimitsConfiguration = perTenantLimitsConfiguration;
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index fc23832..6d08611 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -236,7 +236,7 @@ CREATE TABLE IF NOT EXISTS entity_view (
     customer_id varchar(31),
     type varchar(255),
     name varchar(255),
-    keys varchar(255),
+    keys varchar(10000000),
     start_ts bigint,
     end_ts bigint,
     search_text varchar(255),
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 267d560..944ed66 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -56,6 +56,7 @@ services:
         max-file: "30"
     environment:
       TB_HOST: tb1
+      CLUSTER_NODE_ID: tb1
     env_file:
       - tb-node.env
     volumes:
@@ -77,6 +78,7 @@ services:
         max-file: "30"
     environment:
       TB_HOST: tb2
+      CLUSTER_NODE_ID: tb2
     env_file:
       - tb-node.env
     volumes:
@@ -93,6 +95,7 @@ services:
       - "1883"
     environment:
       TB_HOST: tb-mqtt-transport1
+      CLUSTER_NODE_ID: tb-mqtt-transport1
     env_file:
       - tb-mqtt-transport.env
     volumes:
@@ -107,6 +110,7 @@ services:
       - "1883"
     environment:
       TB_HOST: tb-mqtt-transport2
+      CLUSTER_NODE_ID: tb-mqtt-transport2
     env_file:
       - tb-mqtt-transport.env
     volumes:
@@ -121,6 +125,7 @@ services:
       - "8081"
     environment:
       TB_HOST: tb-http-transport1
+      CLUSTER_NODE_ID: tb-http-transport1
     env_file:
       - tb-http-transport.env
     volumes:
@@ -135,6 +140,7 @@ services:
       - "8081"
     environment:
       TB_HOST: tb-http-transport2
+      CLUSTER_NODE_ID: tb-http-transport2
     env_file:
       - tb-http-transport.env
     volumes:
@@ -149,6 +155,7 @@ services:
       - "5683:5683/udp"
     environment:
       TB_HOST: tb-coap-transport
+      CLUSTER_NODE_ID: tb-coap-transport
     env_file:
       - tb-coap-transport.env
     volumes:

docker/kafka.env 2(+1 -1)

diff --git a/docker/kafka.env b/docker/kafka.env
index 87dad07..485b3c0 100644
--- a/docker/kafka.env
+++ b/docker/kafka.env
@@ -4,7 +4,7 @@ KAFKA_LISTENERS=INSIDE://:9093,OUTSIDE://:9092
 KAFKA_ADVERTISED_LISTENERS=INSIDE://:9093,OUTSIDE://kafka:9092
 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
 KAFKA_INTER_BROKER_LISTENER_NAME=INSIDE
-KAFKA_CREATE_TOPICS=js.eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.rule-engine:30:1
+KAFKA_CREATE_TOPICS=js.eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb.rule-engine:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600
 KAFKA_AUTO_CREATE_TOPICS_ENABLE=false
 KAFKA_LOG_RETENTION_BYTES=1073741824
 KAFKA_LOG_SEGMENT_BYTES=268435456
diff --git a/docker/tb-node.env b/docker/tb-node.env
index ca945ab..963943d 100644
--- a/docker/tb-node.env
+++ b/docker/tb-node.env
@@ -8,3 +8,5 @@ JS_EVALUATOR=remote
 TRANSPORT_TYPE=remote
 CACHE_TYPE=redis
 REDIS_HOST=redis
+
+HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE=false
diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
index 9918963..324f3cf 100644
--- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
+++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
@@ -66,7 +66,7 @@ public class MqttClientTest extends AbstractContainerTest {
 
         WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
         MqttClient mqttClient = getMqttClient(deviceCredentials, null);
-        mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload().toString().getBytes()));
+        mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload().toString().getBytes())).get();
         WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
         log.info("Received telemetry: {}", actualLatestTelemetry);
         wsClient.closeBlocking();
@@ -93,7 +93,7 @@ public class MqttClientTest extends AbstractContainerTest {
 
         WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
         MqttClient mqttClient = getMqttClient(deviceCredentials, null);
-        mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload(ts).toString().getBytes()));
+        mqttClient.publish("v1/devices/me/telemetry", Unpooled.wrappedBuffer(createPayload(ts).toString().getBytes())).get();
         WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
         log.info("Received telemetry: {}", actualLatestTelemetry);
         wsClient.closeBlocking();
@@ -123,7 +123,7 @@ public class MqttClientTest extends AbstractContainerTest {
         clientAttributes.addProperty("attr2", true);
         clientAttributes.addProperty("attr3", 42.0);
         clientAttributes.addProperty("attr4", 73);
-        mqttClient.publish("v1/devices/me/attributes", Unpooled.wrappedBuffer(clientAttributes.toString().getBytes()));
+        mqttClient.publish("v1/devices/me/attributes", Unpooled.wrappedBuffer(clientAttributes.toString().getBytes())).get();
         WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
         log.info("Received telemetry: {}", actualLatestTelemetry);
         wsClient.closeBlocking();
@@ -146,6 +146,7 @@ public class MqttClientTest extends AbstractContainerTest {
         Device device = createDevice("mqtt_");
         DeviceCredentials deviceCredentials = restClient.getCredentials(device.getId());
 
+        WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
         MqttMessageListener listener = new MqttMessageListener();
         MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
 
@@ -153,7 +154,17 @@ public class MqttClientTest extends AbstractContainerTest {
         JsonObject clientAttributes = new JsonObject();
         String clientAttributeValue = RandomStringUtils.randomAlphanumeric(8);
         clientAttributes.addProperty("clientAttr", clientAttributeValue);
-        mqttClient.publish("v1/devices/me/attributes", Unpooled.wrappedBuffer(clientAttributes.toString().getBytes()));
+        mqttClient.publish("v1/devices/me/attributes", Unpooled.wrappedBuffer(clientAttributes.toString().getBytes())).get();
+
+        WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
+        log.info("Received ws telemetry: {}", actualLatestTelemetry);
+        wsClient.closeBlocking();
+
+        Assert.assertEquals(1, actualLatestTelemetry.getData().size());
+        Assert.assertEquals(Sets.newHashSet("clientAttr"),
+                actualLatestTelemetry.getLatestValues().keySet());
+
+        Assert.assertTrue(verify(actualLatestTelemetry, "clientAttr", clientAttributeValue));
 
         // Add a new shared attribute
         JsonObject sharedAttributes = new JsonObject();
@@ -166,12 +177,16 @@ public class MqttClientTest extends AbstractContainerTest {
         Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
 
         // Subscribe to attributes response
-        mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE);
+        mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE).get();
+
+        // Wait until subscription is processed
+        TimeUnit.SECONDS.sleep(3);
+
         // Request attributes
         JsonObject request = new JsonObject();
         request.addProperty("clientKeys", "clientAttr");
         request.addProperty("sharedKeys", "sharedAttr");
-        mqttClient.publish("v1/devices/me/attributes/request/" + new Random().nextInt(100), Unpooled.wrappedBuffer(request.toString().getBytes()));
+        mqttClient.publish("v1/devices/me/attributes/request/" + new Random().nextInt(100), Unpooled.wrappedBuffer(request.toString().getBytes())).get();
         MqttEvent event = listener.getEvents().poll(10, TimeUnit.SECONDS);
         AttributesResponse attributes = mapper.readValue(Objects.requireNonNull(event).getMessage(), AttributesResponse.class);
         log.info("Received telemetry: {}", attributes);
@@ -193,7 +208,10 @@ public class MqttClientTest extends AbstractContainerTest {
 
         MqttMessageListener listener = new MqttMessageListener();
         MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
-        mqttClient.on("v1/devices/me/attributes", listener, MqttQoS.AT_LEAST_ONCE);
+        mqttClient.on("v1/devices/me/attributes", listener, MqttQoS.AT_LEAST_ONCE).get();
+
+        // Wait until subscription is processed
+        TimeUnit.SECONDS.sleep(3);
 
         String sharedAttributeName = "sharedAttr";
 
@@ -236,7 +254,10 @@ public class MqttClientTest extends AbstractContainerTest {
 
         MqttMessageListener listener = new MqttMessageListener();
         MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
-        mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE);
+        mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE).get();
+
+        // Wait until subscription is processed
+        TimeUnit.SECONDS.sleep(3);
 
         // Send an RPC from the server
         JsonObject serverRpcPayload = new JsonObject();
@@ -263,7 +284,7 @@ public class MqttClientTest extends AbstractContainerTest {
         JsonObject clientResponse = new JsonObject();
         clientResponse.addProperty("response", "someResponse");
         // Send a response to the server's RPC request
-        mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes()));
+        mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes())).get();
 
         ResponseEntity serverResponse = future.get(5, TimeUnit.SECONDS);
         Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful());
@@ -280,7 +301,7 @@ public class MqttClientTest extends AbstractContainerTest {
 
         MqttMessageListener listener = new MqttMessageListener();
         MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
-        mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE);
+        mqttClient.on("v1/devices/me/rpc/request/+", listener, MqttQoS.AT_LEAST_ONCE).get();
 
         // Get the default rule chain id to make it root again after test finished
         RuleChainId defaultRuleChainId = getDefaultRuleChainId();
@@ -294,7 +315,7 @@ public class MqttClientTest extends AbstractContainerTest {
         clientRequest.addProperty("method", "getResponse");
         clientRequest.addProperty("params", true);
         Integer requestId = 42;
-        mqttClient.publish("v1/devices/me/rpc/request/" + requestId, Unpooled.wrappedBuffer(clientRequest.toString().getBytes()));
+        mqttClient.publish("v1/devices/me/rpc/request/" + requestId, Unpooled.wrappedBuffer(clientRequest.toString().getBytes())).get();
 
         // Check the response from the server
         TimeUnit.SECONDS.sleep(1);
diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
index b9460b3..2bfdeb6 100644
--- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
+++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
@@ -390,7 +390,9 @@ final class MqttClientImpl implements MqttClient {
     }
 
     public void onSuccessfulReconnect() {
-        callback.onSuccessfulReconnect();
+        if (callback != null) {
+            callback.onSuccessfulReconnect();
+        }
     }
 
 
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 f9d3c64..eec8340 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
@@ -34,6 +34,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
 import org.thingsboard.server.dao.user.UserService;
 
 import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
 
 /**
  * Created by ashvayka on 13.01.18.
@@ -98,4 +99,5 @@ public interface TbContext {
 
     ScriptEngine createJsScriptEngine(String script, String... argNames);
 
+    String getNodeId();
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java
new file mode 100644
index 0000000..dc26016
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.action;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.rule.engine.api.util.TbNodeUtils;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.session.SessionMsgType;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "message count",
+        configClazz = TbMsgCountNodeConfiguration.class,
+        nodeDescription = "Count incoming messages",
+        nodeDetails = "Count incoming messages for specified interval and produces POST_TELEMETRY_REQUEST msg with messages count",
+        icon = "functions",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeMsgCountConfig"
+)
+public class TbMsgCountNode implements TbNode {
+
+    private static final String TB_MSG_COUNT_NODE_MSG = "TbMsgCountNodeMsg";
+
+    private AtomicLong messagesProcessed = new AtomicLong(0);
+    private final Gson gson = new Gson();
+    private UUID nextTickId;
+    private long delay;
+    private String telemetryPrefix;
+    private long lastScheduledTs;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        TbMsgCountNodeConfiguration config = TbNodeUtils.convert(configuration, TbMsgCountNodeConfiguration.class);
+        this.delay = TimeUnit.SECONDS.toMillis(config.getInterval());
+        this.telemetryPrefix = config.getTelemetryPrefix();
+        scheduleTickMsg(ctx);
+
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        if (msg.getType().equals(TB_MSG_COUNT_NODE_MSG) && msg.getId().equals(nextTickId)) {
+            JsonObject telemetryJson = new JsonObject();
+            telemetryJson.addProperty(this.telemetryPrefix + "_" + ctx.getNodeId(), messagesProcessed.longValue());
+
+            messagesProcessed = new AtomicLong(0);
+
+            TbMsgMetaData metaData = new TbMsgMetaData();
+            metaData.putValue("delta", Long.toString(System.currentTimeMillis() - lastScheduledTs + delay));
+
+            TbMsg tbMsg = new TbMsg(UUIDs.timeBased(), SessionMsgType.POST_TELEMETRY_REQUEST.name(), ctx.getTenantId(), metaData, TbMsgDataType.JSON, gson.toJson(telemetryJson), null, null, 0L);
+            ctx.tellNext(tbMsg, SUCCESS);
+            scheduleTickMsg(ctx);
+        } else {
+            messagesProcessed.incrementAndGet();
+        }
+    }
+
+    private void scheduleTickMsg(TbContext ctx) {
+        long curTs = System.currentTimeMillis();
+        if (lastScheduledTs == 0L) {
+            lastScheduledTs = curTs;
+        }
+        lastScheduledTs = lastScheduledTs + delay;
+        long curDelay = Math.max(0L, (lastScheduledTs - curTs));
+        TbMsg tickMsg = ctx.newMsg(TB_MSG_COUNT_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
+        nextTickId = tickMsg.getId();
+        ctx.tellSelf(tickMsg, curDelay);
+    }
+
+    @Override
+    public void destroy() {
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNodeConfiguration.java
new file mode 100644
index 0000000..5c854ec
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNodeConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbMsgCountNodeConfiguration implements NodeConfiguration<TbMsgCountNodeConfiguration> {
+
+    private String telemetryPrefix;
+    private int interval;
+
+    @Override
+    public TbMsgCountNodeConfiguration defaultConfiguration() {
+        TbMsgCountNodeConfiguration configuration = new TbMsgCountNodeConfiguration();
+        configuration.setInterval(1);
+        configuration.setTelemetryPrefix("messageCount");
+        return configuration;
+    }
+}
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 2e0ab78..171bba1 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,4 +1,4 @@
-!function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.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),a=e[t[0]];return function(e,t,r){a.apply(this,[e,t,r].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(79)},function(e,t){},1,1,1,1,function(e,t){e.exports=' <section ng-form name=attributesConfigForm layout=column> <md-input-container class=md-block> <label translate>attribute.attributes-scope</label> <md-select ng-model=configuration.scope ng-disabled=$root.loading> <md-option ng-repeat="scope in types.attributesScope" ng-value=scope.value> {{scope.name | translate}} </md-option> </md-select> </md-input-container> </section> '},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <md-input-container class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <section layout=column layout-gt-sm=row> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section ng-form name=kafkaConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=kafkaConfigForm.topicPattern.$error> <div ng-message=required translate>tb.rulenode.topic-pattern-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bootstrap-servers</label> <input ng-required=true name=bootstrapServers ng-model=configuration.bootstrapServers> <div ng-messages=kafkaConfigForm.bootstrapServers.$error> <div ng-message=required translate>tb.rulenode.bootstrap-servers-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.retries</label> <input type=number step=1 name=retries ng-model=configuration.retries min=0> <div ng-messages=kafkaConfigForm.retries.$error> <div ng-message=min translate>tb.rulenode.min-retries-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.batch-size-bytes</label> <input type=number step=1 name=batchSize ng-model=configuration.batchSize min=0> <div ng-messages=kafkaConfigForm.batchSize.$error> <div ng-message=min translate>tb.rulenode.min-batch-size-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.linger-ms</label> <input type=number step=1 name=linger ng-model=configuration.linger min=0> <div ng-messages=kafkaConfigForm.linger.$error> <div ng-message=min translate>tb.rulenode.min-linger-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.buffer-memory-bytes</label> <input type=number step=1 name=bufferMemory ng-model=configuration.bufferMemory min=0> <div ng-messages=kafkaConfigForm.bufferMemory.$error> <div ng-message=min translate>tb.rulenode.min-buffer-memory-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.acks</label> <md-select ng-model=configuration.acks ng-disabled=$root.loading> <md-option ng-repeat="ackValue in ackValues" ng-value=ackValue> {{ ackValue }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.key-serializer</label> <input ng-required=true name=keySerializer ng-model=configuration.keySerializer> <div ng-messages=kafkaConfigForm.keySerializer.$error> <div ng-message=required translate>tb.rulenode.key-serializer-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.value-serializer</label> <input ng-required=true name=valueSerializer ng-model=configuration.valueSerializer> <div ng-messages=kafkaConfigForm.valueSerializer.$error> <div ng-message=required translate>tb.rulenode.value-serializer-required</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.other-properties</label> <tb-kv-map-config ng-model=configuration.otherProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-mqtt-config ng-form name=mqttConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=mqttConfigForm.topicPattern.$error> <div translate ng-message=required>tb.rulenode.topic-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.mqtt-topic-pattern-hint</div> </md-input-container> <div flex layout=column layout-gt-sm=row> <md-input-container flex=60 class=md-block> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=mqttConfigForm.host.$error> <div translate ng-message=required>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.port> <div ng-messages=mqttConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.port-required</div> <div translate ng-message=min>tb.rulenode.port-range</div> <div translate ng-message=max>tb.rulenode.port-range</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.connect-timeout</label> <input type=number step=1 min=1 max=200 ng-required=true name=connectTimeoutSec ng-model=configuration.connectTimeoutSec> <div ng-messages=mqttConfigForm.connectTimeoutSec.$error> <div translate ng-message=required>tb.rulenode.connect-timeout-required</div> <div translate ng-message=min>tb.rulenode.connect-timeout-range</div> <div translate ng-message=max>tb.rulenode.connect-timeout-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.client-id</label> <input name=clientId ng-model=configuration.clientId> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.clean-session\' | translate }}" ng-model=configuration.cleanSession> {{ \'tb.rulenode.clean-session\' | translate }} </md-checkbox> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-ssl\' | translate }}" ng-model=configuration.ssl> {{ \'tb.rulenode.enable-ssl\' | translate }} </md-checkbox> <md-expansion-panel-group class=tb-credentials-panel-group ng-class="{\'disabled\': $root.loading || readonly}" md-component-id=credentialsPanelGroup> <md-expansion-panel md-component-id=credentialsPanel> <md-expansion-panel-collapsed> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-collapsed> <md-expansion-panel-expanded> <md-expansion-panel-header ng-click="$mdExpansionPanel(\'credentialsPanel\').collapse()"> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-header> <md-expansion-panel-content> <div layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.credentials-type</label> <md-select ng-required=true name=credentialsType ng-model=configuration.credentials.type ng-disabled="$root.loading || readonly" ng-change=credentialsTypeChanged()> <md-option ng-repeat="(credentialsType, credentialsValue) in ruleNodeTypes.mqttCredentialTypes" ng-value=credentialsValue.value> {{credentialsValue.name | translate}} </md-option> </md-select> <div ng-messages=mqttConfigForm.credentialsType.$error> <div translate ng-message=required>tb.rulenode.credentials-type-required</div> </div> </md-input-container> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes.basic.value"> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input ng-required=true name=mqttUsername ng-model=configuration.credentials.username> <div ng-messages=mqttConfigForm.mqttUsername.$error> <div translate ng-message=required>tb.rulenode.username-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input type=password ng-required=true name=mqttPassword ng-model=configuration.credentials.password> <div ng-messages=mqttConfigForm.mqttPassword.$error> <div translate ng-message=required>tb.rulenode.password-required</div> </div> </md-input-container> </section> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes[\'cert.PEM\'].value" class=dropdown-section> <div class=tb-container ng-class="configuration.credentials.caCertFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.ca-cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'caCert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'caCert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=caCertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=caCertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.caCertFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.caCertFileName>{{configuration.credentials.caCertFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.certFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'Cert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'Cert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=CertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=CertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.certFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.certFileName>{{configuration.credentials.certFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.privateKeyFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.private-key</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'privateKey\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'privateKey\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=privateKeySelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=privateKeySelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.privateKeyFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.privateKeyFileName>{{configuration.credentials.privateKeyFileName}}</div> </div> <md-input-container class=md-block> <label translate>tb.rulenode.private-key-password</label> <input type=password name=privateKeyPassword ng-model=configuration.credentials.password> </md-input-container> </section> </div> </md-expansion-panel-content> </md-expansion-panel-expanded> </md-expansion-panel> </md-expansion-panel-group> </section>'},function(e,t){e.exports=" <section ng-form name=msgDelayConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=0> <div ng-messages=msgDelayConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-0-seconds-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-pending-messages</label> <input ng-required=true type=number step=1 name=maxPendingMsgs ng-model=configuration.maxPendingMsgs min=1 max=100000> <div ng-messages=msgDelayConfigForm.maxPendingMsgs.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.max-pending-messages-required</div> <div ng-message=min translate>tb.rulenode.max-pending-messages-range</div> <div ng-message=max translate>tb.rulenode.max-pending-messages-range</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=rabbitMqConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.exchange-name-pattern</label> <input name=exchangeNamePattern ng-model=configuration.exchangeNamePattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.routing-key-pattern</label> <input name=routingKeyPattern ng-model=configuration.routingKeyPattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.message-properties</label> <md-select ng-model=configuration.messageProperties ng-disabled="$root.loading || readonly"> <md-option ng-repeat="property in messageProperties" ng-value=property> {{ property }} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=rabbitMqConfigForm.host.$error> <div ng-message=required translate>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.port</label> <input ng-required=true type=number step=1 name=port ng-model=configuration.port min=0 max=65535> <div ng-messages=rabbitMqConfigForm.port.$error> <div ng-message=required translate>tb.rulenode.port-required</div> <div ng-message=min translate>tb.rulenode.port-range</div> <div ng-message=max translate>tb.rulenode.port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.virtual-host</label> <input name=virtualHost ng-model=configuration.virtualHost> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=virtualHost ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=virtualHost type=password ng-model=configuration.password> </md-input-container> <md-input-container class=md-block> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.automatic-recovery\' | translate }}" ng-model=ruleNode.automaticRecoveryEnabled>{{ \'tb.rulenode.automatic-recovery\' | translate }} </md-checkbox> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.connection-timeout-ms</label> <input type=number step=1 name=connectionTimeout ng-model=configuration.connectionTimeout min=0> <div ng-messages=rabbitMqConfigForm.connectionTimeout.$error> <div ng-message=min translate>tb.rulenode.min-connection-timeout-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.handshake-timeout-ms</label> <input type=number step=1 name=handshakeTimeout ng-model=configuration.handshakeTimeout min=0> <div ng-messages=rabbitMqConfigForm.handshakeTimeout.$error> <div ng-message=min translate>tb.rulenode.min-handshake-timeout-ms-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.client-properties</label> <tb-kv-map-config ng-model=configuration.clientProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=' <section ng-form name=restApiCallConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.endpoint-url-pattern</label> <input ng-required=true name=endpointUrlPattern ng-model=configuration.restEndpointUrlPattern> <div ng-messages=restApiCallConfigForm.endpointUrlPattern.$error> <div ng-message=required translate>tb.rulenode.endpoint-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.endpoint-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.request-method</label> <md-select ng-model=configuration.requestMethod ng-disabled=$root.loading> <md-option ng-repeat="type in ruleNodeTypes.httpRequestType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <label translate class=tb-title>tb.rulenode.headers</label> <div class=tb-hint translate>tb.rulenode.headers-hint</div> <tb-kv-map-config ng-model=configuration.headers ng-required=false key-text="\'tb.rulenode.header\'" key-required-text="\'tb.rulenode.header-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section ng-form name=rpcReplyConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.request-id-metadata-attribute</label> <input name=requestIdMetaDataAttribute ng-model=configuration.requestIdMetaDataAttribute> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=rpcRequestConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-sec</label> <input ng-required=true type=number step=1 name=timeoutInSeconds ng-model=configuration.timeoutInSeconds min=0> <div ng-messages=rpcRequestConfigForm.timeoutInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.timeout-required</div> <div ng-message=min translate>tb.rulenode.min-timeout-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sendEmailConfigForm layout=column> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}" ng-model=configuration.useSystemSmtpSettings> {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} </md-checkbox> <section layout=column ng-if=!configuration.useSystemSmtpSettings> <md-input-container class=md-block> <label translate>tb.rulenode.smtp-protocol</label> <md-select ng-disabled="$root.loading || readonly" ng-model=configuration.smtpProtocol> <md-option ng-repeat="smtpProtocol in smtpProtocols" value={{smtpProtocol}}> {{smtpProtocol.toUpperCase()}} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.smtp-host</label> <input ng-required=true name=smtpHost ng-model=configuration.smtpHost> <div ng-messages=sendEmailConfigForm.smtpHost.$error> <div translate ng-message=required>tb.rulenode.smtp-host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.smtp-port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.smtpPort> <div ng-messages=sendEmailConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.smtp-port-required</div> <div translate ng-message=min>tb.rulenode.smtp-port-range</div> <div translate ng-message=max>tb.rulenode.smtp-port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-msec</label> <input type=number step=1 min=0 ng-required=true name=timeout ng-model=configuration.timeout> <div ng-messages=sendEmailConfigForm.timeout.$error> <div translate ng-message=required>tb.rulenode.timeout-required</div> <div translate ng-message=min>tb.rulenode.min-timeout-msec-message</div> </div> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-tls\' | translate }}" ng-model=configuration.enableTls>{{ \'tb.rulenode.enable-tls\' | translate }}</md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=username placeholder="{{ \'tb.rulenode.enter-username\' | translate }}" ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=password placeholder="{{ \'tb.rulenode.enter-password\' | translate }}" type=password ng-model=configuration.password> </md-input-container> </section> </section> '},function(e,t){e.exports=" <section ng-form name=snsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-arn-pattern</label> <input ng-required=true name=topicArnPattern ng-model=configuration.topicArnPattern> <div ng-messages=snsConfigForm.topicArnPattern.$error> <div ng-message=required translate>tb.rulenode.topic-arn-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.topic-arn-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sqsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.queue-type</label> <md-select ng-model=configuration.queueType ng-disabled="$root.loading || readonly"> <md-option ng-repeat="type in ruleNodeTypes.sqsQueueType" ng-value=type.value> {{ type.name | translate }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.queue-url-pattern</label> <input ng-required=true name=queueUrlPattern ng-model=configuration.queueUrlPattern> <div ng-messages=sqsConfigForm.queueUrlPattern.$error> <div ng-message=required translate>tb.rulenode.queue-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.queue-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block ng-if="configuration.queueType == ruleNodeTypes.sqsQueueType.STANDARD.value"> <label translate>tb.rulenode.delay-seconds</label> <input type=number step=1 name=delaySeconds ng-model=configuration.delaySeconds min=0 max=900> <div ng-messages=sqsConfigForm.delaySeconds.$error> <div ng-message=min translate>tb.rulenode.min-delay-seconds-message</div> <div ng-message=max translate>tb.rulenode.max-delay-seconds-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.message-attributes</label> <div class=tb-hint translate>tb.rulenode.message-attributes-hint</div> <tb-kv-map-config ng-model=configuration.messageAttributes ng-required=false key-text="\'tb.rulenode.name\'" key-required-text="\'tb.rulenode.name-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> '},function(e,t){e.exports=" <section ng-form name=timeseriesConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=timeseriesConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat="direction in types.entitySearchDirection" ng-value=direction> {{ (\'relation.search-direction.\' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder="{{ \'tb.rulenode.unlimited-level\' | translate }}" ng-model=query.maxLevel aria-label="{{ \'tb.rulenode.max-relation-level\' | translate }}"> </md-input-container> </div> <div class=md-caption style=color:rgba(0,0,0,.57) translate>relation.relation-type</div> <tb-relation-type-autocomplete flex hide-label ng-model=query.relationType tb-required=false> </tb-relation-type-autocomplete> <div class="md-caption tb-required" style=color:rgba(0,0,0,.57) translate>device.device-types</div> <tb-entity-subtype-list tb-required=true entity-type=types.entityType.device ng-model=query.deviceTypes> </tb-entity-subtype-list> </section> ';
-},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.device-relations-query</label> <tb-device-relations-query-config style=padding-bottom:15px ng-model=configuration.deviceRelationsQuery> </tb-device-relations-query-config> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section class=tb-telemetry-from-database-config ng-form name=getTelemetryConfigForm layout=column> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <md-input-container style=margin-bottom:38px;margin-top:58px> <label translate class="tb-title no-padding">tb.rulenode.fetch-mode</label> <md-select required ng-model=configuration.fetchMode> <md-option ng-repeat="type in ruleNodeTypes.fetchModeType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value"> <label translate class="tb-title no-padding">tb.rulenode.start-interval</label> <input required type=number step=1 min=1 max=2147483647 name=startInterval ng-model=configuration.startInterval> <div ng-messages=getTelemetryConfigForm.startInterval.$error> <div translate ng-message=required>tb.rulenode.start-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit"> <label translate class="tb-title no-padding">tb.rulenode.start-interval-time-unit</label> <md-select required name=startIntervalTimeUnit aria-label="{{ \'tb.rulenode.start-interval-time-unit\' | translate }}" ng-model=configuration.startIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value"> <label translate class="tb-title no-padding">tb.rulenode.end-interval</label> <input required type=number step=1 min=1 max=2147483647 name=endInterval ng-model=configuration.endInterval> <div ng-messages=getTelemetryConfigForm.endInterval.$error> <div translate ng-message=required>tb.rulenode.end-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit"> <label translate class="tb-title no-padding">tb.rulenode.end-interval-time-unit</label> <md-select required name=endIntervalTimeUnit aria-label="{{ \'tb.rulenode.end-interval-time-unit\' | translate }}" ng-model=configuration.endIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.fields-mapping</label> <tb-kv-map-config ng-model=configuration.fieldsMapping ng-required=true required-text="\'tb.rulenode.fields-mapping-required\'" key-text="\'tb.rulenode.source-field\'" key-required-text="\'tb.rulenode.source-field-required\'" val-text="\'tb.rulenode.target-attribute\'" val-required-text="\'tb.rulenode.target-attribute-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},23,function(e,t){e.exports=" <section ng-form name=checkRelationConfigForm> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select> <tb-entity-type-select style=min-width:100px the-form=checkRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> <tb-entity-autocomplete flex ng-if=configuration.entityType the-form=checkRelationConfigForm tb-required=true entity-type=configuration.entityType ng-model=configuration.entityId> </tb-entity-autocomplete> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" class=required>tb.rulenode.originator-types-filter</label> <tb-entity-type-list flex ng-model=configuration.originatorTypes allowed-entity-types=allowedEntityTypes ignore-authority-filter=true tb-required=true> </tb-entity-type-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.from-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.subject-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.body-template-hint</div> </md-input-container> </section> "},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(6),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(7),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue},r.testDetailsBuildJs=function(e){var n=angular.copy(r.configuration.alarmDetailsBuildJs);a.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(8),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n,a){var r=function(r,i,l,s){var u=o.default;i.html(u),r.types=n,r.originator=null,r.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(r.configuration)}),s.$render=function(){r.configuration=s.$viewValue,r.configuration.originatorId&&r.configuration.originatorType?r.originator={id:r.configuration.originatorId,entityType:r.configuration.originatorType}:r.originator=null,r.$watch("originator",function(e,t){angular.equals(e,t)||(r.originator?(s.$viewValue.originatorId=r.originator.id,s.$viewValue.originatorType=r.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},r.testScript=function(e){var n=angular.copy(r.configuration.jsScript);a.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}r.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(1);var i=n(9),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(56),i=a(r),o=n(40),l=a(o),s=n(43),u=a(s),d=n(42),c=a(d),m=n(41),g=a(m),p=n(46),f=a(p),b=n(51),v=a(b),y=n(52),q=a(y),h=n(50),$=a(h),T=n(45),k=a(T),w=n(54),x=a(w),C=n(55),M=a(C),S=n(49),_=a(S),N=n(47),V=a(N),E=n(53),P=a(E),j=n(48),F=a(j);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",$.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",x.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",_.default).directive("tbActionNodeMqttConfig",V.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",F.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.ackValues=["all","-1","0","1"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(10),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(11),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$mdExpansionPanel=t,a.ruleNodeTypes=n,a.credentialsTypeChanged=function(){var e=a.configuration.credentials.type;a.configuration.credentials={},a.configuration.credentials.type=e,a.updateValidity()},a.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){a.$apply(function(){if(n.target.result){l.$setDirty();var r=n.target.result;r&&r.length>0&&("caCert"==t&&(a.configuration.credentials.caCertFileName=e.name,a.configuration.credentials.caCert=r),"privateKey"==t&&(a.configuration.credentials.privateKeyFileName=e.name,a.configuration.credentials.privateKey=r),"Cert"==t&&(a.configuration.credentials.certFileName=e.name,a.configuration.credentials.cert=r)),a.updateValidity()}})},n.readAsText(e.file)},a.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(a.configuration.credentials.caCertFileName=null,a.configuration.credentials.caCert=null),"privateKey"==e&&(a.configuration.credentials.privateKeyFileName=null,a.configuration.credentials.privateKey=null),"Cert"==e&&(a.configuration.credentials.certFileName=null,a.configuration.credentials.cert=null),a.updateValidity()},a.updateValidity=function(){var e=!0,t=a.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(2);var i=n(12),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(13),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),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)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(14),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(15),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(16),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(17),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.smtpProtocols=["smtp","smtps"],t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(18),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(19),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(20),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(21),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(22),o=a(i)},function(e,t){"use strict";function n(e){var t=function(t,n,a,r){n.html("<div></div>"),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$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 a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(23),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.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)||i.$setViewValue(n.configuration);
-}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(24),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s);var u=186;a.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,u],a.ruleNodeTypes=n,a.aggPeriodTimeUnits={},a.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,a.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,a.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,a.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,a.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}r.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(25),o=a(i);n(3)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(63),i=a(r),o=n(64),l=a(o),s=n(60),u=a(s),d=n(65),c=a(d),m=n(59),g=a(m),p=n(66),f=a(p),b=n(61),v=a(b);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",u.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(26),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(27),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(28),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(29),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(30),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(71),i=a(r),o=n(69),l=a(o),s=n(72),u=a(s),d=n(67),c=a(d),m=n(70),g=a(m);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<a.messageTypes.length;t++)e.push(a.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(a.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;r.html(d),a.selectedMessageType=null,a.messageTypeSearchText=null,a.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}a.transformMessageTypeChip=function(e){var n,a=t("filter")(c,{name:e},!0);return n=a&&a.length?angular.copy(a[0]):{name:e,value:e}},a.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},a.createMessageType=function(e,t){var n=angular.element(t,r)[0].firstElementChild,a=angular.element(n),i=a.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),a.scope().$mdChipsCtrl.appendChip(i.trim()),a.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){a.messageTypesWatch&&(a.messageTypesWatch(),a.messageTypesWatch=null);var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var r=0;r<e.messageTypes.length;r++){var i=e.messageTypes[r];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}a.messageTypes=t,a.messageTypesWatch=a.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:a}}r.$inject=["$compile","$filter","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r,n(4);var i=n(31),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.allowedEntityTypes=[t.entityType.device,t.entityType.asset,t.entityType.tenant,t.entityType.customer,t.entityType.user,t.entityType.dashboard,t.entityType.rulechain,t.entityType.rulenode],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(32),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(33),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(34),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){function i(e){e>-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)}),r.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),r.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=r,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||r.$setViewValue(t.query)}),r.$render=function(){if(r.$viewValue){var e=r.$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),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(35),o=a(i);n(5)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(36),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t){var n=function(n,a,r,i){var l=o.default;a.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(a.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}r.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(37),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(75),i=a(r),o=n(77),l=a(o),s=n(78),u=a(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e,t,n){var a=function(a,r,i,l){var s=o.default;r.html(s),a.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(a.configuration)}),l.$render=function(){a.configuration=l.$viewValue},a.testScript=function(e){var r=angular.copy(a.configuration.jsScript);n.testNodeScript(e,r,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],a.ruleNodeId).then(function(e){a.configuration.jsScript=e,l.$setDirty()})},e(r.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:a}}r.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(38),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}function r(e){var t=function(t,n,a,r){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||r.$setViewValue(t.configuration)}),r.$render=function(){t.configuration=r.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}r.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(39),o=a(i)},function(e,t,n){"use strict";function a(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(82),i=a(r),o=n(68),l=a(o),s=n(62),u=a(s),d=n(76),c=a(d),m=n(44),g=a(m),p=n(58),f=a(p),b=n(74),v=a(b),y=n(57),q=a(y),h=n(73),$=a(h),T=n(81),k=a(T);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",$.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{"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","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","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","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","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.","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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> to substitute variables from metadata","request-method":"Request method",headers:"Headers","headers-hint":"Use <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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","message-attributes":"Message attributes","message-attributes-hint":"Use <code>${metaKeyName}</code> 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","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"},"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 a(e){return e&&e.__esModule?e:{default:e}}function r(e){(0,o.default)(e)}r.$inject=["$translateProvider"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=r;var i=n(80),o=a(i)},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"}},fetchModeType:["FIRST","LAST","ALL"],httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},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"}},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(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].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),r=e[t[0]];return function(e,t,a){r.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(81)},function(e,t){},1,1,1,1,function(e,t){e.exports=' <section ng-form name=attributesConfigForm layout=column> <md-input-container class=md-block> <label translate>attribute.attributes-scope</label> <md-select ng-model=configuration.scope ng-disabled=$root.loading> <md-option ng-repeat="scope in types.attributesScope" ng-value=scope.value> {{scope.name | translate}} </md-option> </md-select> </md-input-container> </section> '},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <md-input-container class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <section layout=column layout-gt-sm=row> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section ng-form name=kafkaConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=kafkaConfigForm.topicPattern.$error> <div ng-message=required translate>tb.rulenode.topic-pattern-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bootstrap-servers</label> <input ng-required=true name=bootstrapServers ng-model=configuration.bootstrapServers> <div ng-messages=kafkaConfigForm.bootstrapServers.$error> <div ng-message=required translate>tb.rulenode.bootstrap-servers-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.retries</label> <input type=number step=1 name=retries ng-model=configuration.retries min=0> <div ng-messages=kafkaConfigForm.retries.$error> <div ng-message=min translate>tb.rulenode.min-retries-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.batch-size-bytes</label> <input type=number step=1 name=batchSize ng-model=configuration.batchSize min=0> <div ng-messages=kafkaConfigForm.batchSize.$error> <div ng-message=min translate>tb.rulenode.min-batch-size-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.linger-ms</label> <input type=number step=1 name=linger ng-model=configuration.linger min=0> <div ng-messages=kafkaConfigForm.linger.$error> <div ng-message=min translate>tb.rulenode.min-linger-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.buffer-memory-bytes</label> <input type=number step=1 name=bufferMemory ng-model=configuration.bufferMemory min=0> <div ng-messages=kafkaConfigForm.bufferMemory.$error> <div ng-message=min translate>tb.rulenode.min-buffer-memory-bytes-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.acks</label> <md-select ng-model=configuration.acks ng-disabled=$root.loading> <md-option ng-repeat="ackValue in ackValues" ng-value=ackValue> {{ ackValue }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.key-serializer</label> <input ng-required=true name=keySerializer ng-model=configuration.keySerializer> <div ng-messages=kafkaConfigForm.keySerializer.$error> <div ng-message=required translate>tb.rulenode.key-serializer-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.value-serializer</label> <input ng-required=true name=valueSerializer ng-model=configuration.valueSerializer> <div ng-messages=kafkaConfigForm.valueSerializer.$error> <div ng-message=required translate>tb.rulenode.value-serializer-required</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.other-properties</label> <tb-kv-map-config ng-model=configuration.otherProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-mqtt-config ng-form name=mqttConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-pattern</label> <input ng-required=true name=topicPattern ng-model=configuration.topicPattern> <div ng-messages=mqttConfigForm.topicPattern.$error> <div translate ng-message=required>tb.rulenode.topic-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.mqtt-topic-pattern-hint</div> </md-input-container> <div flex layout=column layout-gt-sm=row> <md-input-container flex=60 class=md-block> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=mqttConfigForm.host.$error> <div translate ng-message=required>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.port> <div ng-messages=mqttConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.port-required</div> <div translate ng-message=min>tb.rulenode.port-range</div> <div translate ng-message=max>tb.rulenode.port-range</div> </div> </md-input-container> <md-input-container flex=40 class=md-block> <label translate>tb.rulenode.connect-timeout</label> <input type=number step=1 min=1 max=200 ng-required=true name=connectTimeoutSec ng-model=configuration.connectTimeoutSec> <div ng-messages=mqttConfigForm.connectTimeoutSec.$error> <div translate ng-message=required>tb.rulenode.connect-timeout-required</div> <div translate ng-message=min>tb.rulenode.connect-timeout-range</div> <div translate ng-message=max>tb.rulenode.connect-timeout-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.client-id</label> <input name=clientId ng-model=configuration.clientId> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.clean-session\' | translate }}" ng-model=configuration.cleanSession> {{ \'tb.rulenode.clean-session\' | translate }} </md-checkbox> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-ssl\' | translate }}" ng-model=configuration.ssl> {{ \'tb.rulenode.enable-ssl\' | translate }} </md-checkbox> <md-expansion-panel-group class=tb-credentials-panel-group ng-class="{\'disabled\': $root.loading || readonly}" md-component-id=credentialsPanelGroup> <md-expansion-panel md-component-id=credentialsPanel> <md-expansion-panel-collapsed> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-collapsed> <md-expansion-panel-expanded> <md-expansion-panel-header ng-click="$mdExpansionPanel(\'credentialsPanel\').collapse()"> <div class=tb-panel-title>{{ \'tb.rulenode.credentials\' | translate }}</div> <div class=tb-panel-prompt>{{ ruleNodeTypes.mqttCredentialTypes[configuration.credentials.type].name | translate }}</div> <span flex></span> <md-expansion-panel-icon></md-expansion-panel-icon> </md-expansion-panel-header> <md-expansion-panel-content> <div layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.credentials-type</label> <md-select ng-required=true name=credentialsType ng-model=configuration.credentials.type ng-disabled="$root.loading || readonly" ng-change=credentialsTypeChanged()> <md-option ng-repeat="(credentialsType, credentialsValue) in ruleNodeTypes.mqttCredentialTypes" ng-value=credentialsValue.value> {{credentialsValue.name | translate}} </md-option> </md-select> <div ng-messages=mqttConfigForm.credentialsType.$error> <div translate ng-message=required>tb.rulenode.credentials-type-required</div> </div> </md-input-container> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes.basic.value"> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input ng-required=true name=mqttUsername ng-model=configuration.credentials.username> <div ng-messages=mqttConfigForm.mqttUsername.$error> <div translate ng-message=required>tb.rulenode.username-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input type=password ng-required=true name=mqttPassword ng-model=configuration.credentials.password> <div ng-messages=mqttConfigForm.mqttPassword.$error> <div translate ng-message=required>tb.rulenode.password-required</div> </div> </md-input-container> </section> <section flex layout=column ng-if="configuration.credentials.type == ruleNodeTypes.mqttCredentialTypes[\'cert.PEM\'].value" class=dropdown-section> <div class=tb-container ng-class="configuration.credentials.caCertFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.ca-cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'caCert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'caCert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=caCertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=caCertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.caCertFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.caCertFileName>{{configuration.credentials.caCertFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.certFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.cert</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'Cert\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'Cert\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=CertSelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=CertSelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.certFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.certFileName>{{configuration.credentials.certFileName}}</div> </div> <div class=tb-container ng-class="configuration.credentials.privateKeyFileName ? \'ng-valid\' : \'ng-invalid\'"> <label class=tb-label translate>tb.rulenode.private-key</label> <div flow-init={singleFile:true} flow-file-added="certFileAdded($file, \'privateKey\')" class=tb-file-select-container> <div class=tb-file-clear-container> <md-button ng-click="clearCertFile(\'privateKey\')" class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'action.remove\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.remove\' | translate }}" class=material-icons>close</md-icon> </md-button> </div> <div class="alert tb-flow-drop" flow-drop> <label for=privateKeySelect translate>tb.rulenode.drop-file</label> <input class=file-input flow-btn id=privateKeySelect> </div> </div> </div> <div class=dropdown-messages> <div ng-if=!configuration.credentials.privateKeyFileName class=tb-error-message translate>tb.rulenode.no-file</div> <div ng-if=configuration.credentials.privateKeyFileName>{{configuration.credentials.privateKeyFileName}}</div> </div> <md-input-container class=md-block> <label translate>tb.rulenode.private-key-password</label> <input type=password name=privateKeyPassword ng-model=configuration.credentials.password> </md-input-container> </section> </div> </md-expansion-panel-content> </md-expansion-panel-expanded> </md-expansion-panel> </md-expansion-panel-group> </section>'},function(e,t){e.exports=" <section ng-form name=msgCountConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.interval-seconds</label> <input ng-required=true type=number step=1 name=interval ng-model=configuration.interval min=1> <div ng-messages=msgCountConfigForm.interval.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.interval-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-interval-seconds-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.output-timeseries-key-prefix</label> <input ng-required=true name=telemetryPrefix ng-model=configuration.telemetryPrefix> <div ng-messages=msgCountConfigForm.telemetryPrefix.$error> <div translate ng-message=required>tb.rulenode.output-timeseries-key-prefix-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=msgDelayConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=0> <div ng-messages=msgDelayConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-0-seconds-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-pending-messages</label> <input ng-required=true type=number step=1 name=maxPendingMsgs ng-model=configuration.maxPendingMsgs min=1 max=100000> <div ng-messages=msgDelayConfigForm.maxPendingMsgs.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.max-pending-messages-required</div> <div ng-message=min translate>tb.rulenode.max-pending-messages-range</div> <div ng-message=max translate>tb.rulenode.max-pending-messages-range</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=rabbitMqConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.exchange-name-pattern</label> <input name=exchangeNamePattern ng-model=configuration.exchangeNamePattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.routing-key-pattern</label> <input name=routingKeyPattern ng-model=configuration.routingKeyPattern> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.message-properties</label> <md-select ng-model=configuration.messageProperties ng-disabled="$root.loading || readonly"> <md-option ng-repeat="property in messageProperties" ng-value=property> {{ property }} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.host</label> <input ng-required=true name=host ng-model=configuration.host> <div ng-messages=rabbitMqConfigForm.host.$error> <div ng-message=required translate>tb.rulenode.host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.port</label> <input ng-required=true type=number step=1 name=port ng-model=configuration.port min=0 max=65535> <div ng-messages=rabbitMqConfigForm.port.$error> <div ng-message=required translate>tb.rulenode.port-required</div> <div ng-message=min translate>tb.rulenode.port-range</div> <div ng-message=max translate>tb.rulenode.port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.virtual-host</label> <input name=virtualHost ng-model=configuration.virtualHost> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=virtualHost ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=virtualHost type=password ng-model=configuration.password> </md-input-container> <md-input-container class=md-block> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.automatic-recovery\' | translate }}" ng-model=ruleNode.automaticRecoveryEnabled>{{ \'tb.rulenode.automatic-recovery\' | translate }} </md-checkbox> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.connection-timeout-ms</label> <input type=number step=1 name=connectionTimeout ng-model=configuration.connectionTimeout min=0> <div ng-messages=rabbitMqConfigForm.connectionTimeout.$error> <div ng-message=min translate>tb.rulenode.min-connection-timeout-ms-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.handshake-timeout-ms</label> <input type=number step=1 name=handshakeTimeout ng-model=configuration.handshakeTimeout min=0> <div ng-messages=rabbitMqConfigForm.handshakeTimeout.$error> <div ng-message=min translate>tb.rulenode.min-handshake-timeout-ms-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.client-properties</label> <tb-kv-map-config ng-model=configuration.clientProperties ng-required=false key-text="\'tb.rulenode.key\'" key-required-text="\'tb.rulenode.key-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=' <section ng-form name=restApiCallConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.endpoint-url-pattern</label> <input ng-required=true name=endpointUrlPattern ng-model=configuration.restEndpointUrlPattern> <div ng-messages=restApiCallConfigForm.endpointUrlPattern.$error> <div ng-message=required translate>tb.rulenode.endpoint-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.endpoint-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.request-method</label> <md-select ng-model=configuration.requestMethod ng-disabled=$root.loading> <md-option ng-repeat="type in ruleNodeTypes.httpRequestType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <label translate class=tb-title>tb.rulenode.headers</label> <div class=tb-hint translate>tb.rulenode.headers-hint</div> <tb-kv-map-config ng-model=configuration.headers ng-required=false key-text="\'tb.rulenode.header\'" key-required-text="\'tb.rulenode.header-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section ng-form name=rpcReplyConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.request-id-metadata-attribute</label> <input name=requestIdMetaDataAttribute ng-model=configuration.requestIdMetaDataAttribute> </md-input-container> </section> "},function(e,t){e.exports=" <section ng-form name=rpcRequestConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-sec</label> <input ng-required=true type=number step=1 name=timeoutInSeconds ng-model=configuration.timeoutInSeconds min=0> <div ng-messages=rpcRequestConfigForm.timeoutInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.timeout-required</div> <div ng-message=min translate>tb.rulenode.min-timeout-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sendEmailConfigForm layout=column> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.use-system-smtp-settings\' | translate }}" ng-model=configuration.useSystemSmtpSettings> {{ \'tb.rulenode.use-system-smtp-settings\' | translate }} </md-checkbox> <section layout=column ng-if=!configuration.useSystemSmtpSettings> <md-input-container class=md-block> <label translate>tb.rulenode.smtp-protocol</label> <md-select ng-disabled="$root.loading || readonly" ng-model=configuration.smtpProtocol> <md-option ng-repeat="smtpProtocol in smtpProtocols" value={{smtpProtocol}}> {{smtpProtocol.toUpperCase()}} </md-option> </md-select> </md-input-container> <div layout-gt-sm=row> <md-input-container class=md-block flex=100 flex-gt-sm=60> <label translate>tb.rulenode.smtp-host</label> <input ng-required=true name=smtpHost ng-model=configuration.smtpHost> <div ng-messages=sendEmailConfigForm.smtpHost.$error> <div translate ng-message=required>tb.rulenode.smtp-host-required</div> </div> </md-input-container> <md-input-container class=md-block flex=100 flex-gt-sm=40> <label translate>tb.rulenode.smtp-port</label> <input type=number step=1 min=1 max=65535 ng-required=true name=port ng-model=configuration.smtpPort> <div ng-messages=sendEmailConfigForm.port.$error> <div translate ng-message=required>tb.rulenode.smtp-port-required</div> <div translate ng-message=min>tb.rulenode.smtp-port-range</div> <div translate ng-message=max>tb.rulenode.smtp-port-range</div> </div> </md-input-container> </div> <md-input-container class=md-block> <label translate>tb.rulenode.timeout-msec</label> <input type=number step=1 min=0 ng-required=true name=timeout ng-model=configuration.timeout> <div ng-messages=sendEmailConfigForm.timeout.$error> <div translate ng-message=required>tb.rulenode.timeout-required</div> <div translate ng-message=min>tb.rulenode.min-timeout-msec-message</div> </div> </md-input-container> <md-checkbox ng-disabled="$root.loading || readonly" aria-label="{{ \'tb.rulenode.enable-tls\' | translate }}" ng-model=configuration.enableTls>{{ \'tb.rulenode.enable-tls\' | translate }}</md-checkbox> <md-input-container class=md-block> <label translate>tb.rulenode.username</label> <input name=username placeholder="{{ \'tb.rulenode.enter-username\' | translate }}" ng-model=configuration.username> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.password</label> <input name=password placeholder="{{ \'tb.rulenode.enter-password\' | translate }}" type=password ng-model=configuration.password> </md-input-container> </section> </section> '},function(e,t){e.exports=" <section ng-form name=snsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.topic-arn-pattern</label> <input ng-required=true name=topicArnPattern ng-model=configuration.topicArnPattern> <div ng-messages=snsConfigForm.topicArnPattern.$error> <div ng-message=required translate>tb.rulenode.topic-arn-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.topic-arn-pattern-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> "},function(e,t){e.exports=' <section ng-form name=sqsConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.queue-type</label> <md-select ng-model=configuration.queueType ng-disabled="$root.loading || readonly"> <md-option ng-repeat="type in ruleNodeTypes.sqsQueueType" ng-value=type.value> {{ type.name | translate }} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.queue-url-pattern</label> <input ng-required=true name=queueUrlPattern ng-model=configuration.queueUrlPattern> <div ng-messages=sqsConfigForm.queueUrlPattern.$error> <div ng-message=required translate>tb.rulenode.queue-url-pattern-required</div> </div> <div class=tb-hint translate>tb.rulenode.queue-url-pattern-hint</div> </md-input-container> <md-input-container class=md-block ng-if="configuration.queueType == ruleNodeTypes.sqsQueueType.STANDARD.value"> <label translate>tb.rulenode.delay-seconds</label> <input type=number step=1 name=delaySeconds ng-model=configuration.delaySeconds min=0 max=900> <div ng-messages=sqsConfigForm.delaySeconds.$error> <div ng-message=min translate>tb.rulenode.min-delay-seconds-message</div> <div ng-message=max translate>tb.rulenode.max-delay-seconds-message</div> </div> </md-input-container> <label translate class=tb-title>tb.rulenode.message-attributes</label> <div class=tb-hint translate>tb.rulenode.message-attributes-hint</div> <tb-kv-map-config ng-model=configuration.messageAttributes ng-required=false key-text="\'tb.rulenode.name\'" key-required-text="\'tb.rulenode.name-required\'" val-text="\'tb.rulenode.value\'" val-required-text="\'tb.rulenode.value-required\'"> </tb-kv-map-config> <md-input-container class=md-block> <label translate>tb.rulenode.aws-access-key-id</label> <input ng-required=true name=accessKeyId ng-model=configuration.accessKeyId> <div ng-messages=snsConfigForm.accessKeyId.$error> <div ng-message=required translate>tb.rulenode.aws-access-key-id-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-secret-access-key</label> <input ng-required=true name=secretAccessKey ng-model=configuration.secretAccessKey> <div ng-messages=snsConfigForm.secretAccessKey.$error> <div ng-message=required translate>tb.rulenode.aws-secret-access-key-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.aws-region</label> <input ng-required=true name=region ng-model=configuration.region> <div ng-messages=snsConfigForm.region.$error> <div ng-message=required translate>tb.rulenode.aws-region-required</div> </div> </md-input-container> </section> '},function(e,t){e.exports=" <section ng-form name=timeseriesConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=timeseriesConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> ";
+},function(e,t){e.exports=' <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat="direction in types.entitySearchDirection" ng-value=direction> {{ (\'relation.search-direction.\' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder="{{ \'tb.rulenode.unlimited-level\' | translate }}" ng-model=query.maxLevel aria-label="{{ \'tb.rulenode.max-relation-level\' | translate }}"> </md-input-container> </div> <div class=md-caption style=color:rgba(0,0,0,.57) translate>relation.relation-type</div> <tb-relation-type-autocomplete flex hide-label ng-model=query.relationType tb-required=false> </tb-relation-type-autocomplete> <div class="md-caption tb-required" style=color:rgba(0,0,0,.57) translate>device.device-types</div> <tb-entity-subtype-list tb-required=true entity-type=types.entityType.device ng-model=query.deviceTypes> </tb-entity-subtype-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.device-relations-query</label> <tb-device-relations-query-config style=padding-bottom:15px ng-model=configuration.deviceRelationsQuery> </tb-device-relations-query-config> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section class=tb-telemetry-from-database-config ng-form name=getTelemetryConfigForm layout=column> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <md-input-container style=margin-bottom:38px;margin-top:58px> <label translate class="tb-title no-padding">tb.rulenode.fetch-mode</label> <md-select required ng-model=configuration.fetchMode> <md-option ng-repeat="type in ruleNodeTypes.fetchModeType" ng-value=type> {{ type }} </md-option> </md-select> </md-input-container> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value"> <label translate class="tb-title no-padding">tb.rulenode.start-interval</label> <input required type=number step=1 min=1 max=2147483647 name=startInterval ng-model=configuration.startInterval> <div ng-messages=getTelemetryConfigForm.startInterval.$error> <div translate ng-message=required>tb.rulenode.start-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit"> <label translate class="tb-title no-padding">tb.rulenode.start-interval-time-unit</label> <md-select required name=startIntervalTimeUnit aria-label="{{ \'tb.rulenode.start-interval-time-unit\' | translate }}" ng-model=configuration.startIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> <div layout=column layout-gt-sm=row> <md-input-container flex class="md-block tb-time-value"> <label translate class="tb-title no-padding">tb.rulenode.end-interval</label> <input required type=number step=1 min=1 max=2147483647 name=endInterval ng-model=configuration.endInterval> <div ng-messages=getTelemetryConfigForm.endInterval.$error> <div translate ng-message=required>tb.rulenode.end-interval-value-required</div> <div ng-message=min translate>tb.rulenode.time-value-range</div> <div ng-message=max translate>tb.rulenode.time-value-range</div> </div> </md-input-container> <md-input-container flex class="md-block tb-time-unit"> <label translate class="tb-title no-padding">tb.rulenode.end-interval-time-unit</label> <md-select required name=endIntervalTimeUnit aria-label="{{ \'tb.rulenode.end-interval-time-unit\' | translate }}" ng-model=configuration.endIntervalTimeUnit> <md-option ng-repeat="timeUnit in ruleNodeTypes.timeUnit" ng-value=timeUnit.value> {{timeUnit.name | translate}} </md-option> </md-select> </md-input-container> </div> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title tb-required">tb.rulenode.fields-mapping</label> <tb-kv-map-config ng-model=configuration.fieldsMapping ng-required=true required-text="\'tb.rulenode.fields-mapping-required\'" key-text="\'tb.rulenode.source-field\'" key-required-text="\'tb.rulenode.source-field-required\'" val-text="\'tb.rulenode.target-attribute\'" val-required-text="\'tb.rulenode.target-attribute-required\'"> </tb-kv-map-config> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},24,function(e,t){e.exports=" <section ng-form name=checkRelationConfigForm> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=configuration.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <div layout=row class=tb-entity-select> <tb-entity-type-select style=min-width:100px the-form=checkRelationConfigForm tb-required=true ng-model=configuration.entityType> </tb-entity-type-select> <tb-entity-autocomplete flex ng-if=configuration.entityType the-form=checkRelationConfigForm tb-required=true entity-type=configuration.entityType ng-model=configuration.entityId> </tb-entity-autocomplete> </div> <tb-relation-type-autocomplete hide-label ng-model=configuration.relationType tb-required=true> </tb-relation-type-autocomplete> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" class=required>tb.rulenode.originator-types-filter</label> <tb-entity-type-list flex ng-model=configuration.originatorTypes allowed-entity-types=allowedEntityTypes ignore-authority-filter=true tb-required=true> </tb-entity-type-list> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.from-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> <div class=tb-hint translate>tb.rulenode.mail-address-list-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.subject-template-hint</div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> <div class=tb-hint translate>tb.rulenode.body-template-hint</div> </md-input-container> </section> "},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(6),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),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);r.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(i.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 i=n(7),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),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);r.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(i.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 i=n(8),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),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);r.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(i.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 i=n(9),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(58),i=r(a),o=n(41),l=r(o),s=n(44),u=r(s),d=n(43),c=r(d),m=n(42),g=r(m),p=n(47),f=r(p),b=n(53),v=r(b),y=n(54),q=r(y),h=n(52),$=r(h),T=n(46),k=r(T),x=n(56),w=r(x),C=n(57),M=r(C),S=n(51),_=r(S),N=n(48),V=r(N),E=n(55),P=r(E),j=n(50),F=r(j),A=n(49),O=r(A);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTimeseriesConfig",i.default).directive("tbActionNodeAttributesConfig",l.default).directive("tbActionNodeGeneratorConfig",u.default).directive("tbActionNodeCreateAlarmConfig",c.default).directive("tbActionNodeClearAlarmConfig",g.default).directive("tbActionNodeLogConfig",f.default).directive("tbActionNodeRpcReplyConfig",v.default).directive("tbActionNodeRpcRequestConfig",q.default).directive("tbActionNodeRestApiCallConfig",$.default).directive("tbActionNodeKafkaConfig",k.default).directive("tbActionNodeSnsConfig",w.default).directive("tbActionNodeSqsConfig",M.default).directive("tbActionNodeRabbitMqConfig",_.default).directive("tbActionNodeMqttConfig",V.default).directive("tbActionNodeSendEmailConfig",P.default).directive("tbActionNodeMsgDelayConfig",F.default).directive("tbActionNodeMsgCountConfig",O.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(10),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(11),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$mdExpansionPanel=t,r.ruleNodeTypes=n,r.credentialsTypeChanged=function(){var e=r.configuration.credentials.type;r.configuration.credentials={},r.configuration.credentials.type=e,r.updateValidity()},r.certFileAdded=function(e,t){var n=new FileReader;n.onload=function(n){r.$apply(function(){if(n.target.result){l.$setDirty();var a=n.target.result;a&&a.length>0&&("caCert"==t&&(r.configuration.credentials.caCertFileName=e.name,r.configuration.credentials.caCert=a),"privateKey"==t&&(r.configuration.credentials.privateKeyFileName=e.name,r.configuration.credentials.privateKey=a),"Cert"==t&&(r.configuration.credentials.certFileName=e.name,r.configuration.credentials.cert=a)),r.updateValidity()}})},n.readAsText(e.file)},r.clearCertFile=function(e){l.$setDirty(),"caCert"==e&&(r.configuration.credentials.caCertFileName=null,r.configuration.credentials.caCert=null),"privateKey"==e&&(r.configuration.credentials.privateKeyFileName=null,r.configuration.credentials.privateKey=null),"Cert"==e&&(r.configuration.credentials.certFileName=null,r.configuration.credentials.cert=null),r.updateValidity()},r.updateValidity=function(){var e=!0,t=r.configuration.credentials;t.type==n.mqttCredentialTypes["cert.PEM"].value&&(t.caCert&&t.cert&&t.privateKey||(e=!1)),l.$setValidity("Certs",e)},r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$mdExpansionPanel","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var i=n(12),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(13),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(14),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(15),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(16),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(17),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(18),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(19),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(20),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.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 i=n(21),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(22),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(23),o=r(i)},function(e,t){"use strict";function n(e){var t=function(t,n,r,a){n.html("<div></div>"),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 r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(24),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(25),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s);var u=186;r.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,u],r.ruleNodeTypes=n,r.aggPeriodTimeUnits={},r.aggPeriodTimeUnits.MINUTES=n.timeUnit.MINUTES,r.aggPeriodTimeUnits.HOURS=n.timeUnit.HOURS,r.aggPeriodTimeUnits.DAYS=n.timeUnit.DAYS,r.aggPeriodTimeUnits.MILLISECONDS=n.timeUnit.MILLISECONDS,r.aggPeriodTimeUnits.SECONDS=n.timeUnit.SECONDS,r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$mdConstant","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(26),o=r(i);n(3)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(65),i=r(a),o=n(66),l=r(o),s=n(62),u=r(s),d=n(67),c=r(d),m=n(61),g=r(m),p=n(68),f=r(p),b=n(63),v=r(b);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeOriginatorFieldsConfig",l.default).directive("tbEnrichmentNodeDeviceAttributesConfig",u.default).directive("tbEnrichmentNodeRelatedAttributesConfig",c.default).directive("tbEnrichmentNodeCustomerAttributesConfig",g.default).directive("tbEnrichmentNodeTenantAttributesConfig",f.default).directive("tbEnrichmentNodeGetTelemetryFromDatabase",v.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.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)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(27),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(28),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(29),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(30),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(31),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(73),i=r(a),o=n(71),l=r(o),s=n(74),u=r(s),d=n(69),c=r(d),m=n(72),g=r(m);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).directive("tbFilterNodeCheckRelationConfig",c.default).directive("tbFilterNodeOriginatorTypeConfig",g.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<r.messageTypes.length;t++)e.push(r.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(r.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;a.html(d),r.selectedMessageType=null,r.messageTypeSearchText=null,r.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}r.transformMessageTypeChip=function(e){var n,r=t("filter")(c,{name:e},!0);return n=r&&r.length?angular.copy(r[0]):{name:e,value:e}},r.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},r.createMessageType=function(e,t){var n=angular.element(t,a)[0].firstElementChild,r=angular.element(n),i=r.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),r.scope().$mdChipsCtrl.appendChip(i.trim()),r.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){r.messageTypesWatch&&(r.messageTypesWatch(),r.messageTypesWatch=null);var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var a=0;a<e.messageTypes.length;a++){var i=e.messageTypes[a];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}r.messageTypes=t,r.messageTypesWatch=r.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$filter","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(4);var i=n(32),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.allowedEntityTypes=[t.entityType.device,t.entityType.asset,t.entityType.tenant,t.entityType.customer,t.entityType.user,t.entityType.dashboard,t.entityType.rulechain,t.entityType.rulenode],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(33),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(34),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(35),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){function i(e){e>-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),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=a,t.removeKeyVal=i,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),u()},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 i=n(36),o=r(i);n(5)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(37),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(38),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(77),i=r(a),o=n(79),l=r(o),s=n(80),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript);n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],r.ruleNodeId).then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{ruleNodeId:"="},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(39),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),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 i=n(40),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(84),i=r(a),o=n(70),l=r(o),s=n(64),u=r(s),d=n(78),c=r(d),m=n(45),g=r(m),p=n(60),f=r(p),b=n(76),v=r(b),y=n(59),q=r(y),h=n(75),$=r(h),T=n(83),k=r(T);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbNodeEmptyConfig",f.default).directive("tbRelationsQueryConfig",v.default).directive("tbDeviceRelationsQueryConfig",q.default).directive("tbKvMapConfig",$.default).config(k.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{"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","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","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","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","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.","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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> to substitute variables from metadata","body-template":"Body Template","body-template-required":"Body Template is required","body-template-hint":"Mail body template, use <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> to substitute variables from metadata","request-method":"Request method",headers:"Headers","headers-hint":"Use <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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 <code>${metaKeyName}</code> 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","message-attributes":"Message attributes","message-attributes-hint":"Use <code>${metaKeyName}</code> 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","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."},"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 r(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 i=n(82),o=r(i)},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"}},fetchModeType:["FIRST","LAST","ALL"],httpRequestType:["GET","POST","PUT","DELETE"],sqsQueueType:{STANDARD:{name:"tb.rulenode.sqs-queue-standard",value:"STANDARD"},FIFO:{name:"tb.rulenode.sqs-queue-fifo",value:"FIFO"}},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"}},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/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml
index 95e7b75..17f996e 100644
--- a/transport/coap/src/main/resources/tb-coap-transport.yml
+++ b/transport/coap/src/main/resources/tb-coap-transport.yml
@@ -17,7 +17,12 @@
 spring.main.web-environment: false
 spring.main.web-application-type: none
 
-# MQTT server parameters
+# Clustering properties
+cluster:
+  # Unique id for this node (autogenerated if empty)
+  node_id: "${CLUSTER_NODE_ID:}"
+
+# COAP server parameters
 transport:
   coap:
     bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml
index bb1d3c4..31e0194 100644
--- a/transport/http/src/main/resources/tb-http-transport.yml
+++ b/transport/http/src/main/resources/tb-http-transport.yml
@@ -20,6 +20,11 @@ server:
   # Server bind port
   port: "${HTTP_BIND_PORT:8081}"
 
+# Clustering properties
+cluster:
+  # Unique id for this node (autogenerated if empty)
+  node_id: "${CLUSTER_NODE_ID:}"
+
 # HTTP server parameters
 transport:
   http:
diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
index 719530c..46e9eb9 100644
--- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
+++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml
@@ -17,6 +17,11 @@
 spring.main.web-environment: false
 spring.main.web-application-type: none
 
+# Clustering properties
+cluster:
+  # Unique id for this node (autogenerated if empty)
+  node_id: "${CLUSTER_NODE_ID:}"
+
 # MQTT server parameters
 transport:
   mqtt:
diff --git a/ui/src/app/api/telemetry-websocket.service.js b/ui/src/app/api/telemetry-websocket.service.js
index 3e08e60..a598774 100644
--- a/ui/src/app/api/telemetry-websocket.service.js
+++ b/ui/src/app/api/telemetry-websocket.service.js
@@ -26,7 +26,7 @@ const WS_IDLE_TIMEOUT = 90000;
 const MAX_PUBLISH_COMMANDS = 10;
 
 /*@ngInject*/
-function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, types, userService) {
+function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, $mdUtil, $log, toast, types, userService) {
 
     var isOpening = false,
         isOpened = false,
@@ -111,7 +111,11 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
         }
     }
 
-    function onError (/*message*/) {
+    function onError (errorEvent) {
+        if (errorEvent) {
+            //showWsError(0, errorEvent);
+            $log.warn('WebSocket error event', errorEvent);
+        }
         isOpening = false;
     }
 
@@ -137,7 +141,10 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
         }
     }
 
-    function onClose () {
+    function onClose (closeEvent) {
+        if (closeEvent && closeEvent.code > 1000 && closeEvent.code !== 1006) {
+            showWsError(closeEvent.code, closeEvent.reason);
+        }
         isOpening = false;
         isOpened = false;
         if (isActive) {
@@ -162,7 +169,9 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
     function onMessage (message) {
         if (message.data) {
             var data = angular.fromJson(message.data);
-            if (data.subscriptionId) {
+            if (data.errorCode) {
+                showWsError(data.errorCode, data.errorMsg);
+            } else if (data.subscriptionId) {
                 var subscriber = subscribers[data.subscriptionId];
                 if (subscriber && data) {
                     var keys = fetchKeys(data.subscriptionId);
@@ -182,6 +191,18 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
         checkToClose();
     }
 
+    function showWsError(errorCode, errorMsg) {
+        var message = 'WebSocket Error: ';
+        if (errorMsg) {
+            message += errorMsg;
+        } else {
+            message += "error code - " + errorCode + ".";
+        }
+        $mdUtil.nextTick(function () {
+            toast.showError(message);
+        });
+    }
+
     function fetchKeys(subscriptionId) {
         var command = commands[subscriptionId];
         if (command && command.keys && command.keys.length > 0) {
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 506ef1d..bbfca60 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -23,7 +23,9 @@ export default angular.module('thingsboard.types', [])
                 permissionDenied: 20,
                 invalidArguments: 30,
                 badRequestParams: 31,
-                itemNotFound: 32
+                itemNotFound: 32,
+                tooManyRequests: 33,
+                tooManyUpdates: 34
             },
             entryPoints: {
                 login: "/api/auth/login",
diff --git a/ui/src/app/services/toast.scss b/ui/src/app/services/toast.scss
index 952a42d..cea79d0 100644
--- a/ui/src/app/services/toast.scss
+++ b/ui/src/app/services/toast.scss
@@ -16,18 +16,21 @@
 
 md-toast.tb-info-toast .md-toast-content {
   height: 100%;
+  max-height: 100%;
   padding: 18px;
   font-size: 18px;
 }
 
 md-toast.tb-success-toast .md-toast-content {
   height: 100%;
+  max-height: 100%;
   font-size: 18px !important;
   background-color: #008000;
 }
 
 md-toast.tb-error-toast .md-toast-content {
   height: 100%;
+  max-height: 100%;
   font-size: 18px !important;
   background-color: #800000;
 }