thingsboard-aplcache

Changes

application/pom.xml 10(+10 -0)

common/pom.xml 2(+1 -1)

dao/pom.xml 2(+1 -1)

pom.xml 12(+12 -0)

resume.bat 18(+18 -0)

tools/pom.xml 2(+1 -1)

ui/package.json 1(+1 -0)

ui/pom.xml 2(+1 -1)

Details

application/pom.xml 10(+10 -0)

diff --git a/application/pom.xml b/application/pom.xml
index 890da91..c7d3240 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -496,6 +496,16 @@
                                     <artifactId>extension-mqtt</artifactId>
                                     <classifier>extension</classifier>
                                 </artifactItem>
+                                <artifactItem>
+                                    <groupId>org.thingsboard.extensions</groupId>
+                                    <artifactId>extension-sqs</artifactId>
+                                    <classifier>extension</classifier>
+                                </artifactItem>
+                                <artifactItem>
+                                    <groupId>org.thingsboard.extensions</groupId>
+                                    <artifactId>extension-sns</artifactId>
+                                    <classifier>extension</classifier>
+                                </artifactItem>
                             </artifactItems>
                         </configuration>
                     </execution>
diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
index 164366d..d811393 100644
--- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
@@ -18,7 +18,7 @@
         "controllerScript": "self.onInit = function() {\n    var scope = self.ctx.$scope;\n    var id = self.ctx.$scope.$injector.get('utils').guid();\n    scope.tableId = \"table-\"+id;\n    scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n    self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n    return {\n        'actionCellButton': {\n            name: 'widget-action.action-cell-button',\n            multiple: true\n        },\n        'rowClick': {\n            name: 'widget-action.row-click',\n            multiple: false\n        }\n    };\n}\n\nself.onDestroy = function() {\n}\n",
         "settingsSchema": "{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"AlarmTableSettings\",\n        \"properties\": {\n            \"alarmsTitle\": {\n                \"title\": \"Alarms table title\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"enableSelection\": {\n                \"title\": \"Enable alarms selection\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"enableSearch\": {\n                \"title\": \"Enable alarms search\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"displayDetails\": {\n                \"title\": \"Display alarm details\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"allowAcknowledgment\": {\n                \"title\": \"Allow alarms acknowledgment\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"allowClear\": {\n                \"title\": \"Allow alarms clear\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"displayPagination\": {\n                \"title\": \"Display pagination\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"defaultPageSize\": {\n                \"title\": \"Default page size\",\n                \"type\": \"number\",\n                \"default\": 10\n            },\n            \"defaultSortOrder\": {\n                \"title\": \"Default sort order\",\n                \"type\": \"string\",\n                \"default\": \"-createdTime\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"alarmsTitle\",\n        \"enableSelection\",\n        \"enableSearch\",\n        \"displayDetails\",\n        \"allowAcknowledgment\",\n        \"allowClear\",\n        \"displayPagination\",\n        \"defaultPageSize\",\n        \"defaultSortOrder\"\n    ]\n}",
         "dataKeySettingsSchema": "{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"columnWidth\": {\n                \"title\": \"Column width (px or %)\",\n                \"type\": \"string\",\n                \"default\": \"0px\"\n            },\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"useCellContentFunction\": {\n                \"title\": \"Use cell content function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellContentFunction\": {\n                \"title\": \"Cell content function: f(value, alarm, filter)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"columnWidth\",\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        },\n        \"useCellContentFunction\",\n        {\n            \"key\": \"cellContentFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}",
-        "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5}"
+        "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5}"
       }
     }
   ]
diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 034209e..0fc0085 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -34,7 +34,7 @@
         "controllerScript": "self.onInit = function() {\n    var scope = self.ctx.$scope;\n    var id = self.ctx.$scope.$injector.get('utils').guid();\n    scope.tableId = \"table-\"+id;\n    scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n    self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1,\n        dataKeysOptional: true\n    };\n}\n\nself.actionSources = function() {\n    return {\n        'actionCellButton': {\n            name: 'widget-action.action-cell-button',\n            multiple: true\n        },\n        'rowClick': {\n            name: 'widget-action.row-click',\n            multiple: false\n        }\n    };\n}\n\nself.onDestroy = function() {\n}\n",
         "settingsSchema": "{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"EntitiesTableSettings\",\n        \"properties\": {\n            \"entitiesTitle\": {\n                \"title\": \"Entities table title\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"enableSearch\": {\n                \"title\": \"Enable entities search\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"displayEntityName\": {\n                \"title\": \"Display entity name column\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"entityNameColumnTitle\": {\n                \"title\": \"Entity name column title\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"displayEntityType\": {\n                \"title\": \"Display entity type column\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"displayPagination\": {\n                \"title\": \"Display pagination\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"defaultPageSize\": {\n                \"title\": \"Default page size\",\n                \"type\": \"number\",\n                \"default\": 10\n            },\n            \"defaultSortOrder\": {\n                \"title\": \"Default sort order\",\n                \"type\": \"string\",\n                \"default\": \"entityName\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"entitiesTitle\",\n        \"enableSearch\",\n        \"displayEntityName\",\n        \"entityNameColumnTitle\",\n        \"displayEntityType\",\n        \"displayPagination\",\n        \"defaultPageSize\",\n        \"defaultSortOrder\"\n    ]\n}",
         "dataKeySettingsSchema": "{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"columnWidth\": {\n                \"title\": \"Column width (px or %)\",\n                \"type\": \"string\",\n                \"default\": \"0px\"\n            },\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"useCellContentFunction\": {\n                \"title\": \"Use cell content function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellContentFunction\": {\n                \"title\": \"Cell content function: f(value, entity, filter)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"columnWidth\",\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        },\n        \"useCellContentFunction\",\n        {\n            \"key\": \"cellContentFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}",
-        "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}"
+        "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}]}"
       }
     },
     {
diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json
new file mode 100644
index 0000000..c963835
--- /dev/null
+++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json
@@ -0,0 +1,25 @@
+{
+  "widgetsBundle": {
+    "alias": "gateway_widgets",
+    "title": "Gateway widgets",
+    "image": null
+  },
+  "widgetTypes": [
+    {
+      "alias": "extension_configuration_widget",
+      "name": "Extensions table",
+      "descriptor": {
+        "type": "latest",
+        "sizeX": 9,
+        "sizeY": 6.5,
+        "resources": [],
+        "templateHtml": "<tb-extensions-table-widget \n    ctx=\"ctx\">\n</tb-extensions-table-widget>",
+        "templateCss": "#container {\n    overflow: auto;\n}",
+        "controllerScript": "self.onInit = function() {\n    var scope = self.ctx.$scope;\n    scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n    return {\n        maxDatasources: 1\n    };\n}\n\nself.onDestroy = function() {\n}\n",
+        "settingsSchema": "{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"ExtensionTableSettings\",\n        \"properties\": {\n            \"extensionsTitle\": {\n                \"title\": \"Extension table title\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"extensionsTitle\"\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\":\"4px\",\"settings\":{},\"title\":\"Extensions table\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
index c695ec8..7a7e3ff 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
@@ -170,7 +170,7 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
             Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd);
             if (ruleToPluginMsgOptional.isPresent()) {
                 RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get();
-                logger.debug("[{}] Device msg is converter to: {}", entityId, ruleToPluginMsg);
+                logger.debug("[{}] Device msg is converted to: {}", entityId, ruleToPluginMsg);
                 context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self());
                 if (action.isOneWayAction()) {
                     pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS);
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 6395798..6755408 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.http.SessionCreationPolicy;
@@ -148,6 +149,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
     }
 
     @Override
+    public void configure(WebSecurity web) throws Exception {
+        web.ignoring().antMatchers("/static/**");
+    }
+
+    @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.headers().cacheControl().and().frameOptions().disable()
                 .and()
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index 0246e07..cb2f5b9 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -19,8 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.extern.slf4j.Slf4j;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
@@ -30,7 +28,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.exception.ThingsboardErrorCode;
 import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.service.mail.MailService;
@@ -78,9 +75,10 @@ public class AuthController extends BaseController {
     @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
     @ResponseStatus(value = HttpStatus.OK)
     public void changePassword (
-            @RequestParam(value = "currentPassword") String currentPassword,
-            @RequestParam(value = "newPassword") String newPassword) throws ThingsboardException {
+            @RequestBody JsonNode changePasswordRequest) throws ThingsboardException {
         try {
+            String currentPassword = changePasswordRequest.get("currentPassword").asText();
+            String newPassword = changePasswordRequest.get("newPassword").asText();
             SecurityUser securityUser = getCurrentUser();
             UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId());
             if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
@@ -118,9 +116,10 @@ public class AuthController extends BaseController {
     @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
     @ResponseStatus(value = HttpStatus.OK)
     public void requestResetPasswordByEmail (
-            @RequestParam(value = "email") String email,
+            @RequestBody JsonNode resetPasswordByEmailRequest,
             HttpServletRequest request) throws ThingsboardException {
         try {
+            String email = resetPasswordByEmailRequest.get("email").asText();
             UserCredentials userCredentials = userService.requestPasswordReset(email);
             String baseUrl = constructBaseUrl(request);
             String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
@@ -158,10 +157,11 @@ public class AuthController extends BaseController {
     @ResponseStatus(value = HttpStatus.OK)
     @ResponseBody
     public JsonNode activateUser(
-            @RequestParam(value = "activateToken") String activateToken,
-            @RequestParam(value = "password") String password,
+            @RequestBody JsonNode activateRequest,
             HttpServletRequest request) throws ThingsboardException {
         try {
+            String activateToken = activateRequest.get("activateToken").asText();
+            String password = activateRequest.get("password").asText();
             String encodedPassword = passwordEncoder.encode(password);
             UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
             User user = userService.findUserById(credentials.getUserId());
@@ -194,10 +194,11 @@ public class AuthController extends BaseController {
     @ResponseStatus(value = HttpStatus.OK)
     @ResponseBody
     public JsonNode resetPassword(
-            @RequestParam(value = "resetToken") String resetToken,
-            @RequestParam(value = "password") String password,
+            @RequestBody JsonNode resetPasswordRequest,
             HttpServletRequest request) throws ThingsboardException {
         try {
+            String resetToken = resetPasswordRequest.get("resetToken").asText();
+            String password = resetPasswordRequest.get("password").asText();
             UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken);
             if (userCredentials != null) {
                 String encodedPassword = passwordEncoder.encode(password);
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index 689f316..19e4329 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -221,7 +221,10 @@ public abstract class AbstractControllerTest {
         doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
                 .andExpect(status().isSeeOther())
                 .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
-        JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", password).andExpect(status().isOk()), JsonNode.class);
+        JsonNode activateRequest = new ObjectMapper().createObjectNode()
+                .put("activateToken", TestMailService.currentActivateToken)
+                .put("password", password);
+        JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
         validateAndSetJwtToken(tokenInfo, user.getEmail());
         return savedUser;
     }
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
index 8dac038..e3c87f6 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
 
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.Assert;
 import org.junit.Test;
@@ -73,7 +74,11 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
         .andExpect(status().isSeeOther())
         .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
 
-        JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", "testPassword").andExpect(status().isOk()), JsonNode.class);
+        JsonNode activateRequest = new ObjectMapper().createObjectNode()
+                .put("activateToken", TestMailService.currentActivateToken)
+                .put("password", "testPassword");
+
+        JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
         validateAndSetJwtToken(tokenInfo, email);
 
         doGet("/api/auth/user")
@@ -117,13 +122,21 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
         
         User savedUser = createUserAndLogin(user, "testPassword1");
         logout();
-        doPost("/api/noauth/resetPasswordByEmail", "email", email)
+
+        JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode()
+                .put("email", email);
+
+        doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
         .andExpect(status().isOk());
         doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
         .andExpect(status().isSeeOther())
         .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
-        
-        JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", "resetToken", TestMailService.currentResetPasswordToken, "password", "testPassword2").andExpect(status().isOk()), JsonNode.class);
+
+        JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode()
+                .put("resetToken", TestMailService.currentResetPasswordToken)
+                .put("password", "testPassword2");
+
+        JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", resetPasswordRequest).andExpect(status().isOk()), JsonNode.class);
         validateAndSetJwtToken(tokenInfo, email);
 
         doGet("/api/auth/user")
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 2c9603b..11e62e8 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Common Data</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 3f36cdf..33466a6 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Common Messages</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

common/pom.xml 2(+1 -1)

diff --git a/common/pom.xml b/common/pom.xml
index 0320a4a..dfba528 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -28,7 +28,7 @@
     <packaging>pom</packaging>
 
     <name>Thingsboard Server Commons</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index f609e9a..884f455 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Common Transport components</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

dao/pom.xml 2(+1 -1)

diff --git a/dao/pom.xml b/dao/pom.xml
index 9805b6b..d9463e4 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server DAO Layer</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index 3574e83..86afdb4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -384,7 +384,7 @@ public class BaseRelationService implements RelationService {
         Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
         Set<EntityId> childrenIds = new HashSet<>();
         for (EntityRelation childRelation : children) {
-            log.info("Found Relation: {}", childRelation);
+            log.trace("Found Relation: {}", childRelation);
             EntityId childId;
             if (direction == EntitySearchDirection.FROM) {
                 childId = childRelation.getTo();
@@ -392,9 +392,9 @@ public class BaseRelationService implements RelationService {
                 childId = childRelation.getFrom();
             }
             if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
-                log.info("Adding Relation: {}", childId);
+                log.trace("Adding Relation: {}", childId);
                 if (childrenIds.add(childId)) {
-                    log.info("Added Relation: {}", childId);
+                    log.trace("Added Relation: {}", childId);
                 }
             }
         }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
index 7c34aa1..7828267 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
@@ -22,6 +22,7 @@ import javax.annotation.PreDestroy;
 import java.util.concurrent.Executors;
 
 public abstract class JpaAbstractDaoListeningExecutorService {
+
     protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
 
     @PreDestroy
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 7fddfae..170f9a6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
@@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.*;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.PageRequest;
@@ -36,10 +34,12 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
 import org.thingsboard.server.dao.util.SqlDao;
 
 import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@@ -50,6 +50,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
 @SqlDao
 public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
 
+    private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+
     @Autowired
     private TsKvRepository tsKvRepository;
 
@@ -232,7 +234,8 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
         entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
         entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
         entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
-        return service.submit(() -> {
+        log.trace("Saving entity: " + entity);
+        return insertService.submit(() -> {
             tsKvRepository.save(entity);
             return null;
         });
@@ -240,7 +243,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
 
     @Override
     public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
-        return service.submit(() -> null);
+        return insertService.submit(() -> null);
     }
 
     @Override
@@ -254,10 +257,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
         latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
         latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null));
         latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
-        return service.submit(() -> {
+        return insertService.submit(() -> {
             tsKvLatestRepository.save(latestEntity);
             return null;
         });
     }
 
+    @PreDestroy
+    void onDestroy() {
+        insertService.shutdown();
+    }
+
 }
diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index 6342be4..c53fc72 100644
--- a/extensions/extension-kafka/pom.xml
+++ b/extensions/extension-kafka/pom.xml
@@ -30,7 +30,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Kafka Extension</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions/extension-mqtt/pom.xml b/extensions/extension-mqtt/pom.xml
index 85f511d..517fbd3 100644
--- a/extensions/extension-mqtt/pom.xml
+++ b/extensions/extension-mqtt/pom.xml
@@ -30,7 +30,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server MQTT Extension</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index ba5e6d2..9f95670 100644
--- a/extensions/extension-rabbitmq/pom.xml
+++ b/extensions/extension-rabbitmq/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server RabbitMQ Extension</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml
index af5d196..a7872b8 100644
--- a/extensions/extension-rest-api-call/pom.xml
+++ b/extensions/extension-rest-api-call/pom.xml
@@ -30,7 +30,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server REST API Call Extension</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions/extension-sns/pom.xml b/extensions/extension-sns/pom.xml
new file mode 100644
index 0000000..ae31773
--- /dev/null
+++ b/extensions/extension-sns/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>extensions</artifactId>
+        <groupId>org.thingsboard</groupId>
+        <version>1.4.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.thingsboard.extensions</groupId>
+    <artifactId>extension-sns</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server SNS Extension</name>
+    <url>https://thingsboard.io</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+        <aws.sdk.version>1.11.229</aws.sdk.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard</groupId>
+            <artifactId>extensions-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard</groupId>
+            <artifactId>extensions-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.amazonaws</groupId>
+            <artifactId>aws-java-sdk-sns</artifactId>
+            <version>${aws.sdk.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <descriptors>
+                        <descriptor>src/assembly/extension.xml</descriptor>
+                    </descriptors>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/extensions/extension-sns/src/assembly/extension.xml b/extensions/extension-sns/src/assembly/extension.xml
new file mode 100644
index 0000000..2395d55
--- /dev/null
+++ b/extensions/extension-sns/src/assembly/extension.xml
@@ -0,0 +1,37 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
+    <id>extension</id>
+    <formats>
+        <format>jar</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <dependencySets>
+        <dependencySet>
+            <outputDirectory>/</outputDirectory>
+            <useProjectArtifact>true</useProjectArtifact>
+            <unpack>true</unpack>
+            <scope>runtime</scope>
+            <excludes>
+
+            </excludes>
+        </dependencySet>
+    </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java
new file mode 100644
index 0000000..b64a22b
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+public class SnsTopicActionMsg extends AbstractRuleToPluginMsg<SnsTopicActionPayload> {
+
+    public SnsTopicActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SnsTopicActionPayload payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java
new file mode 100644
index 0000000..a1bf89a
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.io.Serializable;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Data
+@Builder
+public class SnsTopicActionPayload implements Serializable {
+
+    private final String topicArn;
+    private final String msgBody;
+
+    private final Integer requestId;
+    private final MsgType msgType;
+    private final boolean sync;
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java
new file mode 100644
index 0000000..79de23a
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
+
+import java.util.Optional;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Action(name = "SNS Topic Action", descriptor = "SnsTopicActionDescriptor.json", configuration = SnsTopicPluginActionConfiguration.class)
+public class SnsTopicPluginAction extends AbstractTemplatePluginAction<SnsTopicPluginActionConfiguration> {
+
+    @Override
+    protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
+        SnsTopicActionPayload.SnsTopicActionPayloadBuilder builder = SnsTopicActionPayload.builder();
+        builder.msgType(payload.getMsgType());
+        builder.requestId(payload.getRequestId());
+        builder.topicArn(configuration.getTopicArn());
+        builder.msgBody(getMsgBody(ctx, msg));
+        return Optional.of(new SnsTopicActionMsg(msg.getTenantId(),
+                msg.getCustomerId(),
+                msg.getDeviceId(),
+                builder.build()));
+    }
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java
new file mode 100644
index 0000000..468c3dd
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Data
+public class SnsTopicPluginActionConfiguration implements TemplateActionConfiguration {
+
+    private String topicArn;
+    private String template;
+    private boolean sync;
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java
new file mode 100644
index 0000000..c7fd99c
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.plugin;
+
+import com.amazonaws.services.sns.AmazonSNS;
+import com.amazonaws.services.sns.model.PublishRequest;
+import com.amazonaws.services.sns.model.PublishResult;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.sns.action.SnsTopicActionMsg;
+import org.thingsboard.server.extensions.sns.action.SnsTopicActionPayload;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class SnsMessageHandler implements RuleMsgHandler {
+
+    private final AmazonSNS sns;
+
+    @Override
+    public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+        if (msg instanceof SnsTopicActionMsg) {
+            SnsTopicActionPayload payload = ((SnsTopicActionMsg) msg).getPayload();
+            PublishRequest publishRequest = new PublishRequest()
+                    .withTopicArn(payload.getTopicArn())
+                    .withMessage(payload.getMsgBody());
+            sns.publish(publishRequest);
+            if (payload.isSync()) {
+                ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
+                        BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
+            }
+           return;
+        }
+        throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
+
+    }
+
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java
new file mode 100644
index 0000000..86f5de0
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.plugin;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.sns.AmazonSNS;
+import com.amazonaws.services.sns.AmazonSNSClient;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.sns.action.SnsTopicPluginAction;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Plugin(name = "SNS Plugin", actions = {SnsTopicPluginAction.class},
+        descriptor = "SnsPluginDescriptor.json", configuration = SnsPluginConfiguration.class)
+public class SnsPlugin extends AbstractPlugin<SnsPluginConfiguration> {
+
+    private SnsMessageHandler snsMessageHandler;
+    private SnsPluginConfiguration configuration;
+
+    @Override
+    public void init(SnsPluginConfiguration configuration) {
+        this.configuration = configuration;
+        init();
+    }
+
+    private void init() {
+        AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
+        AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
+        AmazonSNS sns = AmazonSNSClient.builder()
+                .withCredentials(credProvider)
+                .withRegion(configuration.getRegion())
+                .build();
+        this.snsMessageHandler = new SnsMessageHandler(sns);
+
+    }
+
+    private void destroy() {
+        this.snsMessageHandler = null;
+    }
+
+    @Override
+    protected RuleMsgHandler getRuleMsgHandler() {
+        return snsMessageHandler;
+    }
+
+    @Override
+    public void resume(PluginContext ctx) {
+        init();
+    }
+
+    @Override
+    public void suspend(PluginContext ctx) {
+        destroy();
+    }
+
+    @Override
+    public void stop(PluginContext ctx) {
+        destroy();
+    }
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java
new file mode 100644
index 0000000..dee78f3
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.plugin;
+
+import lombok.Data;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Data
+public class SnsPluginConfiguration {
+
+    private String accessKeyId;
+    private String secretAccessKey;
+    private String region;
+
+}
diff --git a/extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json b/extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json
new file mode 100644
index 0000000..04e23e7
--- /dev/null
+++ b/extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json
@@ -0,0 +1,30 @@
+{
+  "schema": {
+    "title": "SNS Plugin Configuration",
+    "type": "object",
+    "properties": {
+      "accessKeyId": {
+        "title": "Access Key ID",
+        "type": "string"
+      },
+      "secretAccessKey": {
+        "title": "Secret Access Key",
+        "type": "string"
+      },
+      "region": {
+        "title": "Region",
+        "type": "string"
+      }
+    },
+    "required": [
+      "accessKeyId",
+      "secretAccessKey",
+      "region"
+    ]
+  },
+  "form": [
+    "accessKeyId",
+    "secretAccessKey",
+    "region"
+  ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json b/extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json
new file mode 100644
index 0000000..a8a2793
--- /dev/null
+++ b/extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json
@@ -0,0 +1,34 @@
+{
+  "schema": {
+    "title": "SNS Topic Action Configuration",
+    "type": "object",
+    "properties": {
+      "sync": {
+        "title": "Requires delivery confirmation",
+        "type": "boolean"
+      },
+      "topicArn": {
+        "title": "Topic ARN",
+        "type": "string"
+      },
+      "template": {
+        "title": "Body Template",
+        "type": "string"
+      }
+    },
+    "required": [
+      "sync",
+      "topicArn",
+      "template"
+    ]
+  },
+  "form": [
+    "sync",
+    "topicArn",
+    {
+      "key": "template",
+      "type": "textarea",
+      "rows": 5
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sqs/pom.xml b/extensions/extension-sqs/pom.xml
new file mode 100644
index 0000000..7f3818d
--- /dev/null
+++ b/extensions/extension-sqs/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>extensions</artifactId>
+        <groupId>org.thingsboard</groupId>
+        <version>1.4.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.thingsboard.extensions</groupId>
+    <artifactId>extension-sqs</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server SQS Extension</name>
+    <url>https://thingsboard.io</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+        <aws.sdk.version>1.11.229</aws.sdk.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard</groupId>
+            <artifactId>extensions-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard</groupId>
+            <artifactId>extensions-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.amazonaws</groupId>
+            <artifactId>aws-java-sdk-sqs</artifactId>
+            <version>${aws.sdk.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <descriptors>
+                        <descriptor>src/assembly/extension.xml</descriptor>
+                    </descriptors>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/assembly/extension.xml b/extensions/extension-sqs/src/assembly/extension.xml
new file mode 100644
index 0000000..2395d55
--- /dev/null
+++ b/extensions/extension-sqs/src/assembly/extension.xml
@@ -0,0 +1,37 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
+    <id>extension</id>
+    <formats>
+        <format>jar</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <dependencySets>
+        <dependencySet>
+            <outputDirectory>/</outputDirectory>
+            <useProjectArtifact>true</useProjectArtifact>
+            <unpack>true</unpack>
+            <scope>runtime</scope>
+            <excludes>
+
+            </excludes>
+        </dependencySet>
+    </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java
new file mode 100644
index 0000000..c465063
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+public class SqsFifoQueueActionMsg extends AbstractRuleToPluginMsg<SqsFifoQueueActionPayload> {
+
+    public SqsFifoQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsFifoQueueActionPayload payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java
new file mode 100644
index 0000000..692cd90
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.io.Serializable;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+@Data
+@Builder
+public class SqsFifoQueueActionPayload implements Serializable {
+
+    private final String queue;
+    private final String msgBody;
+    private final String deviceId;
+
+    private final Integer requestId;
+    private final MsgType msgType;
+    private final boolean sync;
+
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java
new file mode 100644
index 0000000..f107ea1
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginActionConfiguration;
+
+import java.util.Optional;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Action(name = "SQS Fifo Queue Action", descriptor = "SqsFifoQueueActionDescriptor.json", configuration = SqsFifoQueuePluginActionConfiguration.class)
+public class SqsFifoQueuePluginAction extends AbstractTemplatePluginAction<SqsFifoQueuePluginActionConfiguration> {
+
+    @Override
+    protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
+        SqsFifoQueueActionPayload.SqsFifoQueueActionPayloadBuilder builder = SqsFifoQueueActionPayload.builder();
+        builder.msgType(payload.getMsgType());
+        builder.requestId(payload.getRequestId());
+        builder.queue(configuration.getQueue());
+        builder.deviceId(msg.getDeviceId().toString());
+        builder.msgBody(getMsgBody(ctx, msg));
+        return Optional.of(new SqsFifoQueueActionMsg(msg.getTenantId(),
+                msg.getCustomerId(),
+                msg.getDeviceId(),
+                builder.build()));
+    }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java
new file mode 100644
index 0000000..d100325
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+@Data
+public class SqsFifoQueuePluginActionConfiguration implements TemplateActionConfiguration {
+
+    private String queue;
+    private String template;
+    private boolean sync;
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java
new file mode 100644
index 0000000..6666b27
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+public class SqsStandardQueueActionMsg extends AbstractRuleToPluginMsg<SqsStandardQueueActionPayload> {
+
+    public SqsStandardQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsStandardQueueActionPayload payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java
new file mode 100644
index 0000000..ce3dd27
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.io.Serializable;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@Data
+@Builder
+public class SqsStandardQueueActionPayload implements Serializable {
+
+    private final String queue;
+    private final String msgBody;
+    private final int delaySeconds;
+
+    private final Integer requestId;
+    private final MsgType msgType;
+    private final boolean sync;
+
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java
new file mode 100644
index 0000000..1dd9d56
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
+
+import java.util.Optional;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Action(name = "SQS Standard Queue Action", descriptor = "SqsStandardQueueActionDescriptor.json", configuration = SqsStandardQueuePluginActionConfiguration.class)
+public class SqsStandardQueuePluginAction extends AbstractTemplatePluginAction<SqsStandardQueuePluginActionConfiguration> {
+
+    @Override
+    protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
+        SqsStandardQueueActionPayload.SqsStandardQueueActionPayloadBuilder builder = SqsStandardQueueActionPayload.builder();
+        builder.msgType(payload.getMsgType());
+        builder.requestId(payload.getRequestId());
+        builder.queue(configuration.getQueue());
+        builder.delaySeconds(configuration.getDelaySeconds());
+        builder.msgBody(getMsgBody(ctx, msg));
+        return Optional.of(new SqsStandardQueueActionMsg(msg.getTenantId(),
+                msg.getCustomerId(),
+                msg.getDeviceId(),
+                builder.build()));
+    }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java
new file mode 100644
index 0000000..5c21ef5
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@Data
+public class SqsStandardQueuePluginActionConfiguration implements TemplateActionConfiguration {
+
+    private String queue;
+    private int delaySeconds;
+    private boolean sync;
+    private String template;
+
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java
new file mode 100644
index 0000000..b71e0b1
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.plugin;
+
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import com.amazonaws.services.sqs.model.SendMessageResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionMsg;
+import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionPayload;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class SqsMessageHandler implements RuleMsgHandler {
+
+    private final AmazonSQS sqs;
+
+    @Override
+    public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+        if (msg instanceof SqsStandardQueueActionMsg) {
+            sendMessageToStandardQueue(ctx, tenantId, ruleId, msg);
+            return;
+        }
+        if (msg instanceof SqsFifoQueueActionMsg) {
+            sendMessageToFifoQueue(ctx, tenantId, ruleId, msg);
+            return;
+        }
+        throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
+    }
+
+    private void sendMessageToStandardQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
+        SqsStandardQueueActionPayload payload = ((SqsStandardQueueActionMsg) msg).getPayload();
+        SendMessageRequest sendMsgRequest = new SendMessageRequest()
+                .withDelaySeconds(payload.getDelaySeconds())
+                .withQueueUrl(payload.getQueue())
+                .withMessageBody(payload.getMsgBody());
+        sqs.sendMessage(sendMsgRequest);
+        if (payload.isSync()) {
+            ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
+                    BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
+        }
+    }
+
+    private void sendMessageToFifoQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
+        SqsFifoQueueActionPayload payload = ((SqsFifoQueueActionMsg) msg).getPayload();
+        SendMessageRequest sendMsgRequest =  new SendMessageRequest()
+                .withQueueUrl(payload.getQueue())
+                .withMessageBody(payload.getMsgBody())
+                .withMessageGroupId(payload.getDeviceId());
+        sqs.sendMessage(sendMsgRequest);
+        if (payload.isSync()) {
+            ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
+                    BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
+        }
+    }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java
new file mode 100644
index 0000000..3f0252e
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.plugin;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueuePluginAction;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginAction;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@Plugin(name = "SQS Plugin", actions = {SqsStandardQueuePluginAction.class, SqsFifoQueuePluginAction.class},
+        descriptor = "SqsPluginDescriptor.json", configuration = SqsPluginConfiguration.class)
+public class SqsPlugin extends AbstractPlugin<SqsPluginConfiguration> {
+
+    private SqsMessageHandler sqsMessageHandler;
+    private SqsPluginConfiguration configuration;
+
+    @Override
+    public void init(SqsPluginConfiguration configuration) {
+        this.configuration = configuration;
+        init();
+    }
+
+    private void init() {
+        AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
+        AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
+                .withRegion(Regions.fromName(configuration.getRegion())).build();
+        this.sqsMessageHandler = new SqsMessageHandler(sqs);
+
+    }
+
+    private void destroy() {
+        this.sqsMessageHandler = null;
+    }
+
+    @Override
+    protected RuleMsgHandler getRuleMsgHandler() {
+        return sqsMessageHandler;
+    }
+
+    @Override
+    public void resume(PluginContext ctx) {
+        init();
+    }
+
+    @Override
+    public void suspend(PluginContext ctx) {
+        destroy();
+    }
+
+    @Override
+    public void stop(PluginContext ctx) {
+        destroy();
+    }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java
new file mode 100644
index 0000000..a93b7f3
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.plugin;
+
+import lombok.Data;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Data
+public class SqsPluginConfiguration {
+
+    private String accessKeyId;
+    private String secretAccessKey;
+    private String region;
+
+}
diff --git a/extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json b/extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json
new file mode 100644
index 0000000..05bfaec
--- /dev/null
+++ b/extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json
@@ -0,0 +1,34 @@
+{
+  "schema": {
+    "title": "SQS FIFO Queue Action Configuration",
+    "type": "object",
+    "properties": {
+      "sync": {
+        "title": "Requires delivery confirmation",
+        "type": "boolean"
+      },
+      "queue": {
+        "title": "Queue URL",
+        "type": "string"
+      },
+      "template": {
+        "title": "Body Template",
+        "type": "string"
+      }
+    },
+    "required": [
+      "sync",
+      "queue",
+      "template"
+    ]
+  },
+  "form": [
+    "sync",
+    "queue",
+    {
+      "key": "template",
+      "type": "textarea",
+      "rows": 5
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json b/extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json
new file mode 100644
index 0000000..407468b
--- /dev/null
+++ b/extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json
@@ -0,0 +1,30 @@
+{
+  "schema": {
+    "title": "SQS Plugin Configuration",
+    "type": "object",
+    "properties": {
+      "accessKeyId": {
+        "title": "Access Key ID",
+        "type": "string"
+      },
+      "secretAccessKey": {
+        "title": "Secret Access Key",
+        "type": "string"
+      },
+      "region": {
+        "title": "Region",
+        "type": "string"
+      }
+    },
+    "required": [
+      "accessKeyId",
+      "secretAccessKey",
+      "region"
+    ]
+  },
+  "form": [
+    "accessKeyId",
+    "secretAccessKey",
+    "region"
+  ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json b/extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json
new file mode 100644
index 0000000..f5502e6
--- /dev/null
+++ b/extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json
@@ -0,0 +1,41 @@
+{
+  "schema": {
+    "title": "SQS Standard Queue Action Configuration",
+    "type": "object",
+    "properties": {
+      "sync": {
+        "title": "Requires delivery confirmation",
+        "type": "boolean"
+      },
+      "queue": {
+        "title": "Queue URL",
+        "type": "string"
+      },
+      "delaySeconds": {
+        "title": "Delay Seconds",
+        "type": "integer",
+        "default": 0
+      },
+      "template": {
+        "title": "Body Template",
+        "type": "string"
+      }
+    },
+    "required": [
+      "sync",
+      "queue",
+      "delaySeconds",
+      "template"
+    ]
+  },
+  "form": [
+    "sync",
+    "queue",
+    "delaySeconds",
+    {
+      "key": "template",
+      "type": "textarea",
+      "rows": 5
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java b/extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java
new file mode 100644
index 0000000..17c9b08
--- /dev/null
+++ b/extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
+import com.amazonaws.services.sqs.model.DeleteMessageRequest;
+import com.amazonaws.services.sqs.model.Message;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+@Slf4j
+public class SqsDemoClient {
+
+    private static final String ACCESS_KEY_ID = "$ACCES_KEY_ID";
+    private static final String SECRET_ACCESS_KEY = "$SECRET_ACCESS_KEY";
+
+    private static final String QUEUE_URL = "$QUEUE_URL";
+    private static final String REGION = "us-east-1";
+
+    public static void main(String[] args) {
+        log.info("Starting SQS Demo Clinent...");
+        AWSCredentials awsCredentials = new BasicAWSCredentials(ACCESS_KEY_ID, SECRET_ACCESS_KEY);
+        AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
+                .withRegion(Regions.fromName(REGION)).build();
+        SqsDemoClient client = new SqsDemoClient();
+        client.pollMessages(sqs);
+    }
+
+    private void pollMessages(AmazonSQS sqs) {
+        log.info("Polling messages");
+        while (true) {
+            List<Message> messages = sqs.receiveMessage(QUEUE_URL).getMessages();
+            messages.forEach(m -> {
+                log.info("Message Received: " + m.getBody());
+                System.out.println(m.getBody());
+                DeleteMessageRequest deleteMessageRequest = new DeleteMessageRequest(QUEUE_URL, m.getReceiptHandle());
+                sqs.deleteMessage(deleteMessageRequest);
+            });
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                e.printStackTrace();
+            }
+        }
+    }
+}
diff --git a/extensions/extension-sqs/src/test/resources/logback.xml b/extensions/extension-sqs/src/test/resources/logback.xml
new file mode 100644
index 0000000..757aa32
--- /dev/null
+++ b/extensions/extension-sqs/src/test/resources/logback.xml
@@ -0,0 +1,10 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="INFO">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
\ No newline at end of file
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 30c9395..50dca83 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -28,7 +28,7 @@
     <packaging>pom</packaging>
 
     <name>Thingsboard Extensions</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <main.dir>${basedir}/..</main.dir>
@@ -39,6 +39,8 @@
         <module>extension-rest-api-call</module>
         <module>extension-kafka</module>
         <module>extension-mqtt</module>
+        <module>extension-sqs</module>
+        <module>extension-sns</module>
     </modules>
 
 </project>
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index 1305c98..298da9a 100644
--- a/extensions-api/pom.xml
+++ b/extensions-api/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Extensions API</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index f94264e..14f91a1 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Core Extensions</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
index 0b8e992..7e92fc1 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -128,12 +128,16 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
         Optional<Long> interval = request.getLongParamValue("interval");
         Optional<Integer> limit = request.getIntParamValue("limit");
 
+        // If some of these params are specified, they all must be
         if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) {
-            if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) {
+            if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent() || interval.get() < 0) {
                 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
                 return;
             }
-            Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
+
+            // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
+            Aggregation agg = (interval.isPresent() && interval.get() == 0) ? Aggregation.valueOf(Aggregation.NONE.name()) :
+                                                                              Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
 
             List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
                     .collect(Collectors.toList());

pom.xml 12(+12 -0)

diff --git a/pom.xml b/pom.xml
index 1f9a92d..e45fc3e 100755
--- a/pom.xml
+++ b/pom.xml
@@ -351,6 +351,18 @@
                 <version>${project.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.thingsboard.extensions</groupId>
+                <artifactId>extension-sqs</artifactId>
+                <classifier>extension</classifier>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.extensions</groupId>
+                <artifactId>extension-sns</artifactId>
+                <classifier>extension</classifier>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.thingsboard.common</groupId>
                 <artifactId>data</artifactId>
                 <version>${project.version}</version>

resume.bat 18(+18 -0)

diff --git a/resume.bat b/resume.bat
new file mode 100644
index 0000000..c3c0e1d
--- /dev/null
+++ b/resume.bat
@@ -0,0 +1,18 @@
+@REM
+@REM Copyright © 2016-2017 The Thingsboard Authors
+@REM
+@REM Licensed under the Apache License, Version 2.0 (the "License");
+@REM you may not use this file except in compliance with the License.
+@REM You may obtain a copy of the License at
+@REM
+@REM     http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing, software
+@REM distributed under the License is distributed on an "AS IS" BASIS,
+@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@REM See the License for the specific language governing permissions and
+@REM limitations under the License.
+@REM
+
+mvn clean install -rf :application
+

tools/pom.xml 2(+1 -1)

diff --git a/tools/pom.xml b/tools/pom.xml
index f0ade6e..5b74b56 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server Tools</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
index d635dbc..7000496 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
@@ -26,9 +26,16 @@ import org.springframework.http.client.ClientHttpResponse;
 import org.springframework.http.client.support.HttpRequestWrapper;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestTemplate;
+import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 
 import java.io.IOException;
@@ -71,18 +78,40 @@ public class RestClient implements ClientHttpRequestInterceptor {
         }
     }
 
-    public Device createDevice(String name) {
+    public Customer createCustomer(String title) {
+        Customer customer = new Customer();
+        customer.setTitle(title);
+        return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
+    }
+
+    public Device createDevice(String name, String type) {
         Device device = new Device();
         device.setName(name);
+        device.setType(type);
         return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
     }
 
+    public Asset createAsset(String name, String type) {
+        Asset asset = new Asset();
+        asset.setName(name);
+        asset.setType(type);
+        return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
+    }
+
+    public Alarm createAlarm(Alarm alarm) {
+        return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
+    }
 
     public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
         return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
                 customerId.toString(), deviceId.toString()).getBody();
     }
 
+    public Asset assignAsset(CustomerId customerId, AssetId assetId) {
+        return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class,
+                customerId.toString(), assetId.toString()).getBody();
+    }
+
     public DeviceCredentials getCredentials(DeviceId id) {
         return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
     }
@@ -91,11 +120,14 @@ public class RestClient implements ClientHttpRequestInterceptor {
         return restTemplate;
     }
 
+    public String getToken() {
+        return token;
+    }
+
     @Override
     public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
         HttpRequest wrapper = new HttpRequestWrapper(request);
         wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
         return execution.execute(wrapper, bytes);
     }
-
-}
+}
\ No newline at end of file
diff --git a/tools/src/main/python/mqtt-send-telemetry.py b/tools/src/main/python/mqtt-send-telemetry.py
new file mode 100644
index 0000000..8c4263d
--- /dev/null
+++ b/tools/src/main/python/mqtt-send-telemetry.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2016-2017 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import paho.mqtt.client as mqtt
+from time import sleep
+import random
+
+broker="test.mosquitto.org"
+topic_pub='v1/devices/me/telemetry'
+
+
+client = mqtt.Client()
+
+client.username_pw_set("TEST_TOKEN")
+client.connect('127.0.0.1', 1883, 1)
+
+for i in range(5):
+    x = random.randrange(20, 100)
+    print x
+    msg = '{"windSpeed":"'+ str(x) + '"}'
+    client.publish(topic_pub, msg)
+    sleep(0.1)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index c67b2a8..7212023 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard COAP Transport</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 58d5d64..dc5dfa5 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard HTTP Transport</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 414ca9f..d8f5703 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard MQTT Transport</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 2e6abfd..7e4c2ea 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -229,6 +229,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
                 } else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) {
                     deviceSessionCtx.setAllowAttributeResponses();
                     grantedQoSList.add(getMinSupportedQos(reqQoS));
+                } else if (topicName.equals(GATEWAY_ATTRIBUTES_TOPIC)) {
+                    grantedQoSList.add(getMinSupportedQos(reqQoS));
                 } else {
                     log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
                     grantedQoSList.add(FAILURE.value());
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
index 7b527df..7bed03a 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.transport.mqtt.session;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import io.netty.buffer.ByteBuf;
@@ -24,6 +25,7 @@ import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.handler.codec.mqtt.*;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.kv.KvEntry;
 import org.thingsboard.server.common.msg.core.*;
 import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
@@ -35,6 +37,7 @@ import org.thingsboard.server.transport.mqtt.MqttTopics;
 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
 
 import java.nio.charset.Charset;
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -83,7 +86,7 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
                 if (responseMsg.isSuccess()) {
                     MsgType requestMsgType = responseMsg.getRequestMsgType();
                     Integer requestId = responseMsg.getRequestId();
-                    if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+                    if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
                         return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
                     }
                 }
@@ -135,40 +138,43 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
         if (responseData.isPresent()) {
             AttributesKVMsg msg = responseData.get();
             if (msg.getClientAttributes() != null) {
-                msg.getClientAttributes().forEach(v -> addValueToJson(result, "value", v));
+                addValues(result, msg.getClientAttributes());
             }
             if (msg.getSharedAttributes() != null) {
-                msg.getSharedAttributes().forEach(v -> addValueToJson(result, "value", v));
+                addValues(result, msg.getSharedAttributes());
             }
         }
         return createMqttPublishMsg(topic, result);
     }
 
+    private void addValues(JsonObject result, List<AttributeKvEntry> kvList) {
+        if (kvList.size() == 1) {
+            addValueToJson(result, "value", kvList.get(0));
+        } else {
+            JsonObject values;
+            if (result.has("values")) {
+                values = result.get("values").getAsJsonObject();
+            } else {
+                values = new JsonObject();
+                result.add("values", values);
+            }
+            kvList.forEach(value -> addValueToJson(values, value.getKey(), value));
+        }
+    }
+
     private void addValueToJson(JsonObject json, String name, KvEntry entry) {
         switch (entry.getDataType()) {
             case BOOLEAN:
-                Optional<Boolean> booleanValue = entry.getBooleanValue();
-                if (booleanValue.isPresent()) {
-                    json.addProperty(name, booleanValue.get());
-                }
+                entry.getBooleanValue().ifPresent(aBoolean -> json.addProperty(name, aBoolean));
                 break;
             case STRING:
-                Optional<String> stringValue = entry.getStrValue();
-                if (stringValue.isPresent()) {
-                    json.addProperty(name, stringValue.get());
-                }
+                entry.getStrValue().ifPresent(aString -> json.addProperty(name, aString));
                 break;
             case DOUBLE:
-                Optional<Double> doubleValue = entry.getDoubleValue();
-                if (doubleValue.isPresent()) {
-                    json.addProperty(name, doubleValue.get());
-                }
+                entry.getDoubleValue().ifPresent(aDouble -> json.addProperty(name, aDouble));
                 break;
             case LONG:
-                Optional<Long> longValue = entry.getLongValue();
-                if (longValue.isPresent()) {
-                    json.addProperty(name, longValue.get());
-                }
+                entry.getLongValue().ifPresent(aLong -> json.addProperty(name, aLong));
                 break;
         }
     }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index 00d1f0c..d69341c 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -41,10 +41,7 @@ import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload;
@@ -186,24 +183,34 @@ public class GatewaySessionCtx {
         }
     }
 
-    public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException {
-        JsonElement json = validateJsonPayload(gatewaySessionId, mqttMsg.payload());
+    public void onDeviceAttributesRequest(MqttPublishMessage msg) throws AdaptorException {
+        JsonElement json = validateJsonPayload(gatewaySessionId, msg.payload());
         if (json.isJsonObject()) {
             JsonObject jsonObj = json.getAsJsonObject();
             int requestId = jsonObj.get("id").getAsInt();
             String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
             boolean clientScope = jsonObj.get("client").getAsBoolean();
-            String key = jsonObj.get("key").getAsString();
+            Set<String> keys;
+            if (jsonObj.has("key")) {
+                keys = Collections.singleton(jsonObj.get("key").getAsString());
+            } else {
+                JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
+                keys = new HashSet<>();
+                for (JsonElement keyObj : keysArray) {
+                    keys.add(keyObj.getAsString());
+                }
+            }
 
             BasicGetAttributesRequest request;
             if (clientScope) {
-                request = new BasicGetAttributesRequest(requestId, Collections.singleton(key), null);
+                request = new BasicGetAttributesRequest(requestId, keys, null);
             } else {
-                request = new BasicGetAttributesRequest(requestId, null, Collections.singleton(key));
+                request = new BasicGetAttributesRequest(requestId, null, keys);
             }
             GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
             processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
                     new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
+            ack(msg);
         } else {
             throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
         }
@@ -251,7 +258,7 @@ public class GatewaySessionCtx {
     }
 
     private void ack(MqttPublishMessage msg) {
-        if(msg.variableHeader().messageId() > 0) {
+        if (msg.variableHeader().messageId() > 0) {
             writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId()));
         }
     }
diff --git a/transport/pom.xml b/transport/pom.xml
index f9b67e3..63b1fe1 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -28,7 +28,7 @@
     <packaging>pom</packaging>
 
     <name>Thingsboard Server Transport Modules</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <main.dir>${basedir}/..</main.dir>

ui/package.json 1(+1 -0)

diff --git a/ui/package.json b/ui/package.json
index cb8fdbd..81f4574 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -30,6 +30,7 @@
     "angular-material": "1.1.1",
     "angular-material-data-table": "^0.10.9",
     "angular-material-icons": "^0.7.1",
+    "angular-material-expansion-panel": "^0.7.2",
     "angular-messages": "1.5.8",
     "angular-route": "1.5.8",
     "angular-sanitize": "1.5.8",

ui/pom.xml 2(+1 -1)

diff --git a/ui/pom.xml b/ui/pom.xml
index d84eaf5..e840067 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -28,7 +28,7 @@
     <packaging>jar</packaging>
 
     <name>Thingsboard Server UI</name>
-    <url>http://thingsboard.org</url>
+    <url>https://thingsboard.io</url>
 
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js
index 272e4df..74f9587 100644
--- a/ui/src/app/api/login.service.js
+++ b/ui/src/app/api/login.service.js
@@ -65,8 +65,8 @@ function LoginService($http, $q) {
 
     function sendResetPasswordLink(email) {
         var deferred = $q.defer();
-        var url = '/api/noauth/resetPasswordByEmail?email=' + email;
-        $http.post(url, null).then(function success(response) {
+        var url = '/api/noauth/resetPasswordByEmail';
+        $http.post(url, {email: email}).then(function success(response) {
             deferred.resolve(response);
         }, function fail() {
             deferred.reject();
@@ -76,8 +76,8 @@ function LoginService($http, $q) {
 
     function resetPassword(resetToken, password) {
         var deferred = $q.defer();
-        var url = '/api/noauth/resetPassword?resetToken=' + resetToken + '&password=' + password;
-        $http.post(url, null).then(function success(response) {
+        var url = '/api/noauth/resetPassword';
+        $http.post(url, {resetToken: resetToken, password: password}).then(function success(response) {
             deferred.resolve(response);
         }, function fail() {
             deferred.reject();
@@ -87,8 +87,8 @@ function LoginService($http, $q) {
 
     function activate(activateToken, password) {
         var deferred = $q.defer();
-        var url = '/api/noauth/activate?activateToken=' + activateToken + '&password=' + password;
-        $http.post(url, null).then(function success(response) {
+        var url = '/api/noauth/activate';
+        $http.post(url, {activateToken: activateToken, password: password}).then(function success(response) {
             deferred.resolve(response);
         }, function fail() {
             deferred.reject();
@@ -98,8 +98,8 @@ function LoginService($http, $q) {
 
     function changePassword(currentPassword, newPassword) {
         var deferred = $q.defer();
-        var url = '/api/auth/changePassword?currentPassword=' + currentPassword + '&newPassword=' + newPassword;
-        $http.post(url, null).then(function success(response) {
+        var url = '/api/auth/changePassword';
+        $http.post(url, {currentPassword: currentPassword, newPassword: newPassword}).then(function success(response) {
             deferred.resolve(response);
         }, function fail() {
             deferred.reject();
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 1668657..d09387b 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -302,7 +302,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
                     $rootScope.forceFullscreen = true;
                     fetchAllowedDashboardIds();
                 } else if (currentUser.userId) {
-                    getUser(currentUser.userId).then(
+                    getUser(currentUser.userId, true).then(
                         function success(user) {
                             currentUserDetails = user;
                             updateUserLang();
@@ -319,6 +319,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
                         },
                         function fail() {
                             deferred.reject();
+                            logout();
                         }
                     )
                 } else {
@@ -414,19 +415,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
         }
         $http.post(url, user).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
 
-    function getUser(userId) {
+    function getUser(userId, ignoreErrors) {
         var deferred = $q.defer();
         var url = '/api/user/' + userId;
-        $http.get(url).then(function success(response) {
+        $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -436,8 +437,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
         var url = '/api/user/' + userId;
         $http.delete(url).then(function success() {
             deferred.resolve();
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 0df17b7..3c74ecb 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
 import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
+import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
 
 import thingsboardRpcWidgets from '../widget/lib/rpc';
 
@@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant';
 import thingsboardUtils from '../common/utils.service';
 
 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
-    thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
+    thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
     .factory('widgetService', WidgetService)
     .name;
 
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 4a6f3f0..5b19089 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router';
 import angularJwt from 'angular-jwt';
 import 'angular-drag-and-drop-lists';
 import mdDataTable from 'angular-material-data-table';
+import 'angular-material-expansion-panel';
 import ngTouch from 'angular-touch';
 import 'angular-carousel';
 import 'clipboard';
@@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css';
 import 'mdPickers/dist/mdPickers.min.css';
 import 'angular-hotkeys/build/hotkeys.min.css';
 import 'angular-carousel/dist/angular-carousel.min.css';
+import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
 import '../scss/main.scss';
 
 import AppConfig from './app.config';
@@ -103,6 +105,7 @@ angular.module('thingsboard', [
     angularJwt,
     'dndLists',
     mdDataTable,
+    'material.components.expansionPanels',
     ngTouch,
     'angular-carousel',
     'ngclipboard',
diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html
index 30d0483..b02b2e2 100644
--- a/ui/src/app/asset/asset-card.tpl.html
+++ b/ui/src/app/asset/asset-card.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <div flex layout="column" style="margin-top: -10px;">
-    <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+    <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
     <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
     <div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
 </div>
diff --git a/ui/src/app/asset/assign-to-customer.controller.js b/ui/src/app/asset/assign-to-customer.controller.js
index 602e599..3df90ef 100644
--- a/ui/src/app/asset/assign-to-customer.controller.js
+++ b/ui/src/app/asset/assign-to-customer.controller.js
@@ -77,8 +77,8 @@ export default function AssignAssetToCustomerController(customerService, assetSe
 
     function assign() {
         var tasks = [];
-        for (var assetId in assetIds) {
-            tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[assetId]));
+        for (var i=0;i<assetIds.length;i++) {
+            tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[i]));
         }
         $q.all(tasks).then(function () {
             $mdDialog.hide();
diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js
index 64a2c64..c4b2486 100644
--- a/ui/src/app/common/dashboard-utils.service.js
+++ b/ui/src/app/common/dashboard-utils.service.js
@@ -425,12 +425,26 @@ function DashboardUtils(types, utils, timeService) {
         var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
         var ratio = gridSettings.columns / prevColumns;
         layout.gridSettings = gridSettings;
+        var maxRow = 0;
         for (var w in layout.widgets) {
             var widget = layout.widgets[w];
+            maxRow = Math.max(maxRow, widget.row + widget.sizeY);
+        }
+        var newMaxRow = Math.round(maxRow * ratio);
+        for (w in layout.widgets) {
+            widget = layout.widgets[w];
+            if (widget.row + widget.sizeY == maxRow) {
+                widget.row = Math.round(widget.row * ratio);
+                widget.sizeY = newMaxRow - widget.row;
+            } else {
+                widget.row = Math.round(widget.row * ratio);
+                widget.sizeY = Math.round(widget.sizeY * ratio);
+            }
             widget.sizeX = Math.round(widget.sizeX * ratio);
-            widget.sizeY = Math.round(widget.sizeY * ratio);
             widget.col = Math.round(widget.col * ratio);
-            widget.row = Math.round(widget.row * ratio);
+            if (widget.col + widget.sizeX > gridSettings.columns) {
+                widget.sizeX = gridSettings.columns - widget.col;
+            }
         }
     }
 
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 7bac29d..5b3c9e4 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', [])
                     name: "event.type-stats"
                 }
             },
+            extensionType: {
+                http: "HTTP",
+                mqtt: "MQTT",
+                opc: "OPC UA"
+            },
+            extensionValueType: {
+                string: 'value.string',
+                long: 'value.long',
+                double: 'value.double',
+                boolean: 'value.boolean'
+            },
+            extensionTransformerType: {
+                toDouble: 'extension.to-double',
+                custom: 'extension.custom'
+            },
+            mqttConverterTypes: {
+                json: 'extension.converter-json',
+                custom: 'extension.custom'
+            },
+            mqttCredentialTypes: {
+                anonymous:  {
+                    value: "anonymous",
+                    name: "extension.anonymous"
+                },
+                basic: {
+                    value: "basic",
+                    name: "extension.basic"
+                },
+                pem: {
+                    value: "cert.PEM",
+                    name: "extension.pem"
+                }
+            },
+            extensionOpcSecurityTypes: {
+                Basic128Rsa15: "Basic128Rsa15",
+                Basic256: "Basic256",
+                Basic256Sha256: "Basic256Sha256",
+                None: "None"
+            },
+            extensionIdentityType: {
+                anonymous: "extension.anonymous",
+                username: "extension.username"
+            },
+            extensionKeystoreType: {
+                PKCS12: "PKCS12",
+                JKS: "JKS"
+            },
             latestTelemetry: {
                 value: "LATEST_TELEMETRY",
                 name: "attribute.scope-latest-telemetry",
diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
index f27a9a5..28b0734 100644
--- a/ui/src/app/components/confirm-on-exit.directive.js
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
     .name;
 
 /*@ngInject*/
-function ConfirmOnExit($state, $mdDialog, $window, $filter) {
+function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
     return {
         link: function ($scope) {
 
             $window.onbeforeunload = function () {
-                if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
                     return $filter('translate')('confirm-on-exit.message');
                 }
             }
             $scope.$on('$stateChangeStart', function (event, next, current, params) {
-                if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
                     event.preventDefault();
                     var confirm = $mdDialog.confirm()
                         .title($filter('translate')('confirm-on-exit.title'))
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index 6605692..c489c32 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -140,6 +140,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
     vm.widgetLayoutInfo = {
     };
 
+    vm.widgetIds = [];
+
     vm.widgetItemMap = {
         sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
         sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
@@ -233,73 +235,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
         removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef
     });
 
-    watchWidgets();
-
     function onGridsterParentResize() {
         if (gridsterParent.height() && autofillHeight()) {
             updateMobileOpts();
         }
     }
 
-    function watchWidgets() {
-        $scope.widgetsCollectionWatch = $scope.$watchCollection('vm.widgets', function () {
-            if (vm.skipInitialWidgetsWatch) {
-                $timeout(function() { vm.skipInitialWidgetsWatch = false; });
-                return;
-            }
-            var ids = [];
-            for (var i=0;i<vm.widgets.length;i++) {
-                var widget = vm.widgets[i];
-                if (!widget.id) {
-                    widget.id = utils.guid();
-                }
-                ids.push(widget.id);
-                var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
-                if (!layoutInfoObject) {
-                    layoutInfoObject = {
-                        widget: widget
-                    };
-                    Object.defineProperty(layoutInfoObject, 'sizeX', {
-                        get: function() { return widgetSizeX(this.widget) },
-                        set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
-                    });
-                    Object.defineProperty(layoutInfoObject, 'sizeY', {
-                        get: function() { return widgetSizeY(this.widget) },
-                        set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
-                    });
-                    Object.defineProperty(layoutInfoObject, 'row', {
-                        get: function() { return widgetRow(this.widget) },
-                        set: function(newRow) { setWidgetRow(this.widget, newRow)}
-                    });
-                    Object.defineProperty(layoutInfoObject, 'col', {
-                        get: function() { return widgetCol(this.widget) },
-                        set: function(newCol) { setWidgetCol(this.widget, newCol)}
-                    });
-                    vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
-                }
-            }
-            for (var widgetId in vm.widgetLayoutInfo) {
-                if (ids.indexOf(widgetId) === -1) {
-                    delete vm.widgetLayoutInfo[widgetId];
-                }
-            }
-            $mdUtil.nextTick(function () {
-                sortWidgets();
-                if (autofillHeight()) {
-                    updateMobileOpts();
-                }
-            });
-        });
-    }
-
-    function stopWatchWidgets() {
-        if ($scope.widgetsCollectionWatch) {
-            $scope.widgetsCollectionWatch();
-            $scope.widgetsCollectionWatch = null;
-        }
-    }
-
-
     //TODO: widgets visibility
     /*gridsterParent.scroll(function () {
         updateVisibleRect();
@@ -344,30 +285,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
         return isMobileSize;
     }
 
-    $scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {
-        updateMobileOpts();
-    });
-
-    $scope.$watch('vm.isMobile', function () {
-        updateMobileOpts();
-    });
-
-    $scope.$watch('vm.autofillHeight', function () {
-        updateMobileOpts();
-    });
-
-    $scope.$watch('vm.mobileAutofillHeight', function () {
-        updateMobileOpts();
-    });
-
-    $scope.$watch('vm.mobileRowHeight', function () {
-        updateMobileOpts();
-    });
-
-    $scope.$watch('vm.isMobileDisabled', function () {
-        updateMobileOpts();
-    });
-
     $scope.$watch('vm.columns', function () {
         var columns = vm.columns ? vm.columns : 24;
         if (vm.gridsterOpts.columns != columns) {
@@ -381,6 +298,19 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
         }
     });
 
+    $scope.$watch(function() {
+        return $mdMedia('gt-sm') + ',' + vm.isMobile + ',' + vm.isMobileDisabled;
+    }, function() {
+        updateMobileOpts();
+        sortWidgets();
+    });
+
+    $scope.$watch(function() {
+        return vm.autofillHeight + ',' + vm.mobileAutofillHeight + ',' + vm.mobileRowHeight;
+    }, function () {
+        updateMobileOpts();
+    });
+
     $scope.$watch('vm.margins', function () {
         var margins = vm.margins ? vm.margins : [10, 10];
         if (!angular.equals(vm.gridsterOpts.margins, margins)) {
@@ -407,9 +337,70 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
         }
     });
 
+    $scope.$watchCollection('vm.widgets', function () {
+        var ids = [];
+        for (var i=0;i<vm.widgets.length;i++) {
+            var widget = vm.widgets[i];
+            if (!widget.id) {
+                widget.id = utils.guid();
+            }
+            ids.push(widget.id);
+        }
+        ids.sort(function (id1, id2) {
+            return id1.localeCompare(id2);
+        });
+        if (angular.equals(ids, vm.widgetIds)) {
+            return;
+        }
+        vm.widgetIds = ids;
+        for (i=0;i<vm.widgets.length;i++) {
+            widget = vm.widgets[i];
+            var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
+            if (!layoutInfoObject) {
+                layoutInfoObject = {
+                    widget: widget
+                };
+                Object.defineProperty(layoutInfoObject, 'sizeX', {
+                    get: function() { return widgetSizeX(this.widget) },
+                    set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
+                });
+                Object.defineProperty(layoutInfoObject, 'sizeY', {
+                    get: function() { return widgetSizeY(this.widget) },
+                    set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
+                });
+                Object.defineProperty(layoutInfoObject, 'row', {
+                    get: function() { return widgetRow(this.widget) },
+                    set: function(newRow) { setWidgetRow(this.widget, newRow)}
+                });
+                Object.defineProperty(layoutInfoObject, 'col', {
+                    get: function() { return widgetCol(this.widget) },
+                    set: function(newCol) { setWidgetCol(this.widget, newCol)}
+                });
+                vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
+            }
+        }
+        for (var widgetId in vm.widgetLayoutInfo) {
+            if (ids.indexOf(widgetId) === -1) {
+                delete vm.widgetLayoutInfo[widgetId];
+            }
+        }
+        sortWidgets();
+        $mdUtil.nextTick(function () {
+            if (autofillHeight()) {
+                updateMobileOpts();
+            }
+        });
+    });
+
+    $scope.$watch('vm.widgetLayouts', function () {
+        updateMobileOpts();
+        sortWidgets();
+    });
+
     $scope.$on('gridster-resized', function (event, sizes, theGridster) {
         if (checkIsLocalGridsterElement(theGridster)) {
             vm.gridster = theGridster;
+            setupGridster(vm.gridster);
             vm.isResizing = false;
             //TODO: widgets visibility
             //updateVisibleRect(false, true);
@@ -419,6 +410,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
     $scope.$on('gridster-mobile-changed', function (event, theGridster) {
         if (checkIsLocalGridsterElement(theGridster)) {
             vm.gridster = theGridster;
+            setupGridster(vm.gridster);
             detectRowSize(vm.gridster.isMobile).then(
                 function(rowHeight) {
                     if (vm.gridsterOpts.rowHeight != rowHeight) {
@@ -517,18 +509,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
     loadDashboard();
 
     function sortWidgets() {
-        stopWatchWidgets();
         vm.widgets.sort(function (widget1, widget2) {
             var row1 = widgetOrder(widget1);
             var row2 = widgetOrder(widget2);
             var res = row1 - row2;
             if (res === 0) {
-                res = widget1.col - widget2.col;
+                res = widgetCol(widget1) - widgetCol(widget2);
             }
             return res;
         });
-        vm.skipInitialWidgetsWatch = true;
-        watchWidgets();
     }
 
     function reload() {
@@ -1037,6 +1026,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
                         $scope.gridsterScopeWatcher = null;
                         var gridsterScope = gridsterElement.scope();
                         vm.gridster = gridsterScope.gridster;
+                        setupGridster(vm.gridster);
                         if (vm.onInit) {
                             vm.onInit({dashboard: vm});
                         }
@@ -1046,6 +1036,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
         });
     }
 
+    function setupGridster(gridster) {
+        if (gridster) {
+            if (!gridster.origMoveOverlappingItems) {
+                gridster.origMoveOverlappingItems = gridster.moveOverlappingItems;
+                gridster.moveOverlappingItems = () => {};
+            }
+        }
+    }
+
     function loading() {
         return !vm.ignoreLoading && $rootScope.loading;
     }
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index 54a029d..159162a 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -16,7 +16,10 @@
 @import '../../scss/constants';
 
 .tb-details-title {
-  font-size: 1.600rem;
+  font-size: 1.000rem;
+  @media (min-width: $layout-breakpoint-gt-sm) {
+    font-size: 1.600rem;
+  }
   font-weight: 400;
   text-transform: uppercase;
   margin: 20px 8px 0 0;
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 7c36b81..3124dc1 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -22,7 +22,7 @@ tb-js-func {
   border: 1px solid #C0C0C0;
   height: 100%;
   #tb-javascript-input {
-    min-width: 400px;
+    min-width: 200px;
     min-height: 200px;
     width: 100%;
     height: 100%;
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index ee3c2e0..bec7991 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -19,7 +19,7 @@
 	<div layout="row" layout-align="start center" style="height: 40px;">
 		<span style="font-style: italic;">function({{ functionArgsString }}) {</span>
 		<span flex></span>
-		<md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+		<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
 	</div>
 	<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
 		<div flex id="tb-javascript-input"
diff --git a/ui/src/app/device/assign-to-customer.controller.js b/ui/src/app/device/assign-to-customer.controller.js
index c04a200..b58435d 100644
--- a/ui/src/app/device/assign-to-customer.controller.js
+++ b/ui/src/app/device/assign-to-customer.controller.js
@@ -77,8 +77,8 @@ export default function AssignDeviceToCustomerController(customerService, device
 
     function assign() {
         var tasks = [];
-        for (var deviceId in deviceIds) {
-            tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[deviceId]));
+        for (var i=0;i<deviceIds.length;i++) {
+            tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[i]));
         }
         $q.all(tasks).then(function () {
             $mdDialog.hide();
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index 58f4c85..d7cd588 100644
--- a/ui/src/app/device/device-card.tpl.html
+++ b/ui/src/app/device/device-card.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <div flex layout="column" style="margin-top: -10px;">
-    <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+    <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
     <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
     <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
 </div>
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index b8394c7..1ec0134 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -67,4 +67,11 @@
                                entity-type="{{vm.types.entityType.device}}">
             </tb-relation-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
+            <tb-extension-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-name="vm.grid.operatingItem().name"
+                                entity-type="{{vm.types.entityType.device}}">
+            </tb-extension-table>
+        </md-tab>
 </tb-grid>
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
index 6f862ba..3b19e9d 100644
--- a/ui/src/app/entity/attribute/attribute-table.tpl.html
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-content flex class="md-padding tb-absolute-fill" layout="column">
-    <section layout="row" ng-show="!disableAttributeScopeSelection">
+    <section ng-show="!disableAttributeScopeSelection">
         <md-input-container class="md-block" style="width: 200px;">
             <label translate>attribute.attributes-scope</label>
             <md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
@@ -26,7 +26,7 @@
             </md-select>
         </md-input-container>
     </section>
-    <div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
+    <div class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
         <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
                                                                  && !selectedAttributes.length
                                                                  && query.search === null">
diff --git a/ui/src/app/entity/relation/relation-dialog.controller.js b/ui/src/app/entity/relation/relation-dialog.controller.js
index ae50e30..d13b4db 100644
--- a/ui/src/app/entity/relation/relation-dialog.controller.js
+++ b/ui/src/app/entity/relation/relation-dialog.controller.js
@@ -79,10 +79,8 @@ export default function RelationDialogController($scope, $mdDialog, types, entit
     });
 
     function updateEditorSize(element) {
-        var newWidth = 600;
         var newHeight = 200;
-        angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px")
-            .width(newWidth.toString() + "px");
+        angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px");
         vm.editor.resize();
     }
 
diff --git a/ui/src/app/entity/relation/relation-dialog.scss b/ui/src/app/entity/relation/relation-dialog.scss
index 55064df..1bd038b 100644
--- a/ui/src/app/entity/relation/relation-dialog.scss
+++ b/ui/src/app/entity/relation/relation-dialog.scss
@@ -19,7 +19,7 @@
   border: 1px solid #C0C0C0;
   height: 100%;
   #tb-relation-additional-info {
-    min-width: 600px;
+    min-width: 200px;
     min-height: 200px;
     width: 100%;
     height: 100%;
diff --git a/ui/src/app/entity/relation/relation-dialog.tpl.html b/ui/src/app/entity/relation/relation-dialog.tpl.html
index 15b0c7f..8799287 100644
--- a/ui/src/app/entity/relation/relation-dialog.tpl.html
+++ b/ui/src/app/entity/relation/relation-dialog.tpl.html
@@ -15,7 +15,7 @@
     limitations under the License.
 
 -->
-<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 400px;">
+<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 600px;">
     <form name="theForm" ng-submit="vm.save()">
         <md-toolbar>
             <div class="md-toolbar-tools">
diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js
new file mode 100644
index 0000000..fe5825c
--- /dev/null
+++ b/ui/src/app/extension/extension-dialog.controller.js
@@ -0,0 +1,298 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import beautify from 'js-beautify';
+
+const js_beautify =  beautify.js;
+
+/*@ngInject*/
+export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
+
+    var vm = this;
+
+    vm.types = types;
+    vm.isAdd = isAdd;
+    vm.entityType = entityType;
+    vm.entityId = entityId;
+    vm.allExtensions = allExtensions;
+
+
+    if (extension) {
+        vm.extension = angular.copy(extension);
+        editTransformers(vm.extension);
+    } else {
+        vm.extension = {};
+    }
+
+
+    vm.extensionTypeChange = function () {
+
+        if (vm.extension.type === "HTTP") {
+            vm.extension.configuration = {
+                "converterConfigurations": []
+            };
+        }
+        if (vm.extension.type === "MQTT") {
+            vm.extension.configuration = {
+                "brokers": []
+            };
+        }
+        if (vm.extension.type === "OPC UA") {
+            vm.extension.configuration = {
+                "servers": []
+            };
+        }
+    };
+
+    vm.cancel = cancel;
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    vm.save = save;
+    function save() {
+        let $errorElement = angular.element('[name=theForm]').find('.ng-invalid');
+
+        if ($errorElement.length) {
+
+            let $mdDialogScroll = angular.element('md-dialog-content').scrollTop();
+            let $mdDialogTop = angular.element('md-dialog-content').offset().top;
+            let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top;
+
+
+            if ($errorElementTop !== $mdDialogTop) {
+                angular.element('md-dialog-content').animate({
+                    scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50
+                }, 500);
+                $errorElement.eq(0).focus();
+            }
+        } else {
+
+            if(vm.isAdd) {
+                vm.allExtensions.push(vm.extension);
+            } else {
+                var index = vm.allExtensions.indexOf(extension);
+                if(index > -1) {
+                    vm.allExtensions[index] = vm.extension;
+                }
+            }
+
+            $mdDialog.hide();
+            saveTransformers();
+
+            var editedValue = angular.toJson(vm.allExtensions);
+
+            attributeService
+                .saveEntityAttributes(
+                    vm.entityType,
+                    vm.entityId,
+                    types.attributesScope.shared.value,
+                    [{key:"configuration", value:editedValue}]
+                )
+                .then(function success() {
+                });
+
+        }
+    }
+    
+    vm.validateId = function() {
+        var coincidenceArray = vm.allExtensions.filter(function(ext) {
+            return ext.id == vm.extension.id;
+        });
+        if(coincidenceArray.length) {
+            if(!vm.isAdd) {
+                if(coincidenceArray[0].id == extension.id) {
+                    $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
+                } else {
+                    $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
+                }
+            } else {
+                $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
+            }
+        } else {
+            $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
+        }
+    };
+
+    function saveTransformers() {
+        if(vm.extension.type == types.extensionType.http) {
+            var config = vm.extension.configuration.converterConfigurations;
+            if(config && config.length > 0) {
+                for(let i=0;i<config.length;i++) {
+                    for(let j=0;j<config[i].converters.length;j++){
+                        for(let k=0;k<config[i].converters[j].attributes.length;k++){
+                            if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
+                                config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
+                            }
+                            delete config[i].converters[j].attributes[k].transformerType;
+                        }
+                        for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
+                            if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
+                                config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
+                            }
+                            delete config[i].converters[j].timeseries[l].transformerType;
+                        }
+                    }
+                }
+            }
+        }
+        if(vm.extension.type == types.extensionType.mqtt) {
+            var brokers = vm.extension.configuration.brokers;
+            if(brokers && brokers.length > 0) {
+                for(let i=0;i<brokers.length;i++) {
+                    if(brokers[i].mapping && brokers[i].mapping.length > 0) {
+                        for(let j=0;j<brokers[i].mapping.length;j++) {
+                            if(brokers[i].mapping[j].converterType == "json") {
+                                delete brokers[i].mapping[j].converter.nameExp;
+                                delete brokers[i].mapping[j].converter.typeExp;
+                            }
+                            delete brokers[i].mapping[j].converterType;
+                        }
+                    }
+                    if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
+                        for(let j=0;j<brokers[i].connectRequests.length;j++) {
+                            delete brokers[i].connectRequests[j].nameExp;
+                        }
+                    }
+                    if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
+                        for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
+                            delete brokers[i].disconnectRequests[j].nameExp;
+                        }
+                    }
+                    if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
+                        for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+                            delete brokers[i].attributeRequests[j].nameExp;
+                        }
+                        for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+                            delete brokers[i].attributeRequests[j].attrKey;
+                        }
+                        for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+                            delete brokers[i].attributeRequests[j].requestId;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    function editTransformers(extension) {
+        if(extension.type == types.extensionType.http) {
+            var config = extension.configuration.converterConfigurations;
+            for(let i=0;i<config.length;i++) {
+                for(let j=0;j<config[i].converters.length;j++){
+                    for(let k=0;k<config[i].converters[j].attributes.length;k++){
+                        if(config[i].converters[j].attributes[k].transformer){
+                            if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
+                                config[i].converters[j].attributes[k].transformerType = "toDouble";
+                            } else {
+                                config[i].converters[j].attributes[k].transformerType = "custom";
+                                config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
+                            }
+                        }
+                    }
+                    for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
+                        if(config[i].converters[j].timeseries[l].transformer){
+                            if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
+                                config[i].converters[j].timeseries[l].transformerType = "toDouble";
+                            } else {
+                                config[i].converters[j].timeseries[l].transformerType = "custom";
+                                config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        if(extension.type == types.extensionType.mqtt) {
+            var brokers = extension.configuration.brokers;
+            for(let i=0;i<brokers.length;i++) {
+                if(brokers[i].mapping && brokers[i].mapping.length > 0) {
+                    for(let j=0;j<brokers[i].mapping.length;j++) {
+                        if(brokers[i].mapping[j].converter.type == "json") {
+                            if(brokers[i].mapping[j].converter.deviceNameTopicExpression) {
+                                brokers[i].mapping[j].converter.nameExp = "deviceNameTopicExpression";
+                            } else {
+                                brokers[i].mapping[j].converter.nameExp = "deviceNameJsonExpression";
+                            }
+                            if(brokers[i].mapping[j].converter.deviceTypeTopicExpression) {
+                                brokers[i].mapping[j].converter.typeExp = "deviceTypeTopicExpression";
+                            } else {
+                                brokers[i].mapping[j].converter.typeExp = "deviceTypeJsonExpression";
+                            }
+                            brokers[i].mapping[j].converterType = "json";
+                        } else {
+                            brokers[i].mapping[j].converterType = "custom";
+                        }
+                    }
+                }
+                if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
+                    for(let j=0;j<brokers[i].connectRequests.length;j++) {
+                        if(brokers[i].connectRequests[j].deviceNameTopicExpression) {
+                            brokers[i].connectRequests[j].nameExp = "deviceNameTopicExpression";
+                        } else {
+                            brokers[i].connectRequests[j].nameExp = "deviceNameJsonExpression";
+                        }
+                    }
+                }
+                if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
+                    for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
+                        if(brokers[i].disconnectRequests[j].deviceNameTopicExpression) {
+                            brokers[i].disconnectRequests[j].nameExp = "deviceNameTopicExpression";
+                        } else {
+                            brokers[i].disconnectRequests[j].nameExp = "deviceNameJsonExpression";
+                        }
+                    }
+                }
+                if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
+                    for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+                        if(brokers[i].attributeRequests[j].deviceNameTopicExpression) {
+                            brokers[i].attributeRequests[j].nameExp = "deviceNameTopicExpression";
+                        } else {
+                            brokers[i].attributeRequests[j].nameExp = "deviceNameJsonExpression";
+                        }
+                        if(brokers[i].attributeRequests[j].attributeKeyTopicExpression) {
+                            brokers[i].attributeRequests[j].attrKey = "attributeKeyTopicExpression";
+                        } else {
+                            brokers[i].attributeRequests[j].attrKey = "attributeKeyJsonExpression";
+                        }
+                        if(brokers[i].attributeRequests[j].requestIdTopicExpression) {
+                            brokers[i].attributeRequests[j].requestId = "requestIdTopicExpression";
+                        } else {
+                            brokers[i].attributeRequests[j].requestId = "requestIdJsonExpression";
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/*@ngInject*/
+export function ParseToNull() {
+    var linker = function (scope, elem, attrs, ngModel) {
+        ngModel.$parsers.push(function(value) {
+            if(value === "") {
+                return null;
+            }
+            return value;
+        })
+    };
+    return {
+        restrict: "A",
+        link: linker,
+        require: "ngModel"
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-dialog.tpl.html b/ui/src/app/extension/extension-dialog.tpl.html
new file mode 100644
index 0000000..73c33d8
--- /dev/null
+++ b/ui/src/app/extension/extension-dialog.tpl.html
@@ -0,0 +1,81 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
+    <form name="theForm" ng-submit="vm.save()" novalidate>
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <fieldset ng-disabled="loading">
+                        <section flex layout="row">
+                            <md-input-container flex="60" class="md-block" md-is-error="theForm.extensionId.$touched && theForm.extensionId.$invalid">
+                                <label translate>extension.extension-id</label>
+                                <input required name="extensionId" ng-model="vm.extension.id" ng-change="vm.validateId()">
+                                <div ng-messages="theForm.extensionId.$error">
+                                    <div translate ng-message="required">extension.field-required</div>
+                                    <div translate ng-message="uniqueIdValidation">extension.unique-id-required</div>
+                                </div>
+                            </md-input-container>
+
+                            <md-input-container flex="40" class="md-block" md-is-error="theForm.extensionType.$touched && theForm.extensionType.$invalid">
+                                <label translate>extension.extension-type</label>
+
+                                <md-select ng-disabled="!vm.isAdd" required name="extensionType" ng-change="vm.extensionTypeChange()" ng-model="vm.extension.type">
+                                    <md-option ng-repeat="(key,value) in vm.types.extensionType" ng-value="value">
+                                        {{value}}
+                                    </md-option>
+                                </md-select>
+
+                                <div ng-messages="theForm.extensionType.$error">
+                                    <div translate ng-message="required">extension.field-required</div>
+                                </div>
+                            </md-input-container>
+                        </section>
+                        <div tb-extension-form-http config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div>
+                        <div tb-extension-form-mqtt config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.mqtt"></div>
+                        <div tb-extension-form-opc  configuration="vm.extension.configuration"  ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
+                    </fieldset>
+                </md-content>
+            </div>
+        </md-dialog-content>
+
+        <md-dialog-actions layout="row">
+            <md-button type="submit"
+                       class="md-raised md-primary"
+            >
+                {{ (vm.isAdd  ? 'action.add' : 'action.save') | translate }}
+            </md-button>
+
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss
new file mode 100644
index 0000000..97ac717
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form.scss
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.extension-form {
+  li > .md-button {
+    color: rgba(0, 0, 0, 0.7);
+    margin: 0;
+  }
+  .vAccordion--default {
+    margin-top: 0;
+    padding-left: 3px;
+  }
+  .tb-container {
+    width:100%;
+  }
+  .dropdown-messages {
+    .tb-error-message {
+      padding: 5px 0 0 0;
+    }
+  }
+  .dropdown-section {
+    margin-bottom: 30px;
+  }
+  v-pane.inner-invalid > v-pane-header {
+    border-bottom: 2px solid rgb(221,44,0);
+  }
+}
+
+.extension-form.extension-mqtt {
+  md-checkbox{
+    margin-left: 10px;
+  }
+}
+
+.tb-extension-custom-transformer-panel {
+  margin-left: 15px;
+  border: 1px solid #C0C0C0;
+  height: 100%;
+  .tb-extension-custom-transformer {
+    min-width: 600px;
+    min-height: 200px;
+    width: 100%;
+    height: 100%;
+  }
+  .ace_text-input {
+    position:absolute!important
+  }
+}
+
+.extensionDialog {
+  min-width: 1000px;
+}
+
+.tb-container-for-select {
+  height: 58px;
+}
+
+.tb-drop-file-input-hide {
+  height: 200%;
+  display: block;
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
new file mode 100644
index 0000000..37a9444
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
@@ -0,0 +1,147 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/theme/github';
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormHttpTemplate from './extension-form-http.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
+
+    var linker = function(scope, element) {
+
+        var template = $templateCache.get(extensionFormHttpTemplate);
+        element.html(template);
+
+        scope.types = types;
+        scope.theForm = scope.$parent.theForm;
+
+        scope.extensionCustomTransformerOptions = {
+            useWrapMode: false,
+            mode: 'json',
+            showGutter: true,
+            showPrintMargin: true,
+            theme: 'github',
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function(_ace) {
+                _ace.$blockScrolling = 1;
+            }
+        };
+
+
+        scope.addConverterConfig = function() {
+            var newConverterConfig = {converterId:"", converters:[]};
+            scope.converterConfigs.push(newConverterConfig);
+
+            scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
+            scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
+        };
+
+        scope.removeConverterConfig = function(config) {
+            var index = scope.converterConfigs.indexOf(config);
+            if (index > -1) {
+                scope.converterConfigs.splice(index, 1);
+            }
+        };
+
+        scope.addConverter = function(converters) {
+            var newConverter = {
+                deviceNameJsonExpression:"",
+                deviceTypeJsonExpression:"",
+                attributes:[],
+                timeseries:[]
+            };
+            converters.push(newConverter);
+        };
+
+        scope.removeConverter = function(converter, converters) {
+            var index = converters.indexOf(converter);
+            if (index > -1) {
+                converters.splice(index, 1);
+            }
+        };
+
+        scope.addAttribute = function(attributes) {
+            var newAttribute = {type:"", key:"", value:""};
+            attributes.push(newAttribute);
+        };
+
+        scope.removeAttribute = function(attribute, attributes) {
+            var index = attributes.indexOf(attribute);
+            if (index > -1) {
+                attributes.splice(index, 1);
+            }
+        };
+
+
+        if(scope.isAdd) {
+            scope.converterConfigs = scope.config.converterConfigurations;
+            scope.addConverterConfig();
+        } else {
+            scope.converterConfigs = scope.config.converterConfigurations;
+        }
+
+        scope.transformerTypeChange = function(attribute) {
+            attribute.transformer = "";
+        };
+
+        scope.validateTransformer = function (model, editorName) {
+            if(model && model.length) {
+                try {
+                    angular.fromJson(model);
+                    scope.theForm[editorName].$setValidity('transformerJSON', true);
+                } catch(e) {
+                    scope.theForm[editorName].$setValidity('transformerJSON', false);
+                }
+            }
+        };
+
+        scope.collapseValidation = function(index, id) {
+            var invalidState = angular.element('#'+id+':has(.ng-invalid)');
+            if(invalidState.length) {
+                invalidState.addClass('inner-invalid');
+            }
+        };
+
+        scope.expandValidation = function (index, id) {
+            var invalidState = angular.element('#'+id);
+            invalidState.removeClass('inner-invalid');
+        };
+        
+        $compile(element.contents())(scope);
+    };
+
+    return {
+        restrict: "A",
+        link: linker,
+        scope: {
+            config: "=",
+            isAdd: "="
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
new file mode 100644
index 0000000..b39794a
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
@@ -0,0 +1,311 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-card class="extension-form extension-http">
+    <md-card-title>
+        <md-card-title-text>
+            <span translate class="md-headline">extension.configuration</span>
+        </md-card-title-text>
+    </md-card-title>
+    <md-card-content>
+        <v-accordion id="http-converter-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+            <v-pane id="http-converters-pane" expanded="true">
+                <v-pane-header>
+                    {{ 'extension.converter-configurations' | translate }}
+                </v-pane-header>
+                <v-pane-content>
+                    <div ng-if="converterConfigs.length > 0">
+                        <ol class="list-group">
+                            <li class="list-group-item" ng-repeat="(configIndex, config) in converterConfigs">
+                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                           class="md-icon-button"
+                                           ng-click="removeConverterConfig(config)"
+                                           ng-hide="converterConfigs.length < 2"
+                                >
+                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.remove' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+                                <md-card>
+                                    <md-card-content>
+
+                                        <md-input-container class="md-block" md-is-error="theForm['httpConverterId_' + configIndex].$touched && theForm['httpConverterId_' + configIndex].$invalid">
+                                            <label translate>extension.converter-id</label>
+                                            <input required name="httpConverterId_{{configIndex}}" ng-model="config.converterId">
+                                            <div ng-messages="theForm['httpConverterId_' + configIndex].$error">
+                                                <div translate ng-message="required">extension.field-required</div>
+                                            </div>
+                                        </md-input-container>
+                                        <md-input-container class="md-block">
+                                            <label translate>extension.token</label>
+                                            <input name="httpToken" ng-model="config.token" parse-to-null>
+                                        </md-input-container>
+                                        <v-accordion id="http-converters-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="http-converters-pane_{{configIndex}}" expanded="true">
+                                                <v-pane-header>
+                                                    {{ 'extension.converters' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="config.converters.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item"
+                                                                ng-repeat="(converterIndex,converter) in config.converters"
+                                                            >
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                           class="md-icon-button"
+                                                                           ng-click="removeConverter(converter, config.converters)"
+                                                                           ng-hide="config.converters.length < 2"
+                                                                >
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <md-input-container class="md-block" md-is-error="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceNameExp_' + configIndex + converterIndex].$invalid">
+                                                                            <label translate>extension.device-name-expression</label>
+                                                                            <input required name="httpDeviceNameExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceNameJsonExpression">
+                                                                            <div ng-messages="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                        <md-input-container class="md-block" md-is-error="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$invalid">
+                                                                            <label translate>extension.device-type-expression</label>
+                                                                            <input required name="httpDeviceTypeExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceTypeJsonExpression">
+                                                                            <div ng-messages="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+
+                                                                        <v-accordion id="http-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                                                            <v-pane id="http-attributes-pane_{{configIndex}}{{converterIndex}}">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.attributes' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="converter.attributes.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in converter.attributes">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, converter.attributes)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$invalid">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="httpAttributeKey_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.key">
+                                                                                                                <div ng-messages="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$invalid">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="httpAttributeType_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$invalid">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="httpAttributeValue_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.value">
+                                                                                                                <div ng-messages="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+
+
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.transformer</label>
+                                                                                                                <md-select name="httpAttributeTransformer" ng-model="attribute.transformerType" ng-change="transformerTypeChange(attribute)">
+                                                                                                                    <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
+                                                                                                                        {{value | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+
+                                                                                                        <div ng-if='attribute.transformerType == "custom"'>
+                                                                                                            <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+                                                                                                            <div flex class="tb-extension-custom-transformer-panel">
+                                                                                                                <div flex class="tb-extension-custom-transformer"
+                                                                                                                     ui-ace="extensionCustomTransformerOptions"
+                                                                                                                     ng-model="attribute.transformer"
+                                                                                                                     name="attributeCustomTransformer_{{configIndex}}{{converterIndex}}{{attributeIndex}}"
+                                                                                                                     ng-change='validateTransformer(attribute.transformer,"attributeCustomTransformer_" + configIndex + converterIndex + attributeIndex)'
+                                                                                                                     required>
+                                                                                                                </div>
+                                                                                                            </div>
+                                                                                                            <div class="tb-error-messages" ng-messages="theForm['attributeCustomTransformer_' + configIndex + converterIndex + attributeIndex].$error" role="alert">
+                                                                                                                <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
+                                                                                                                <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
+                                                                                                            </div>
+                                                                                                        </div>
+
+
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(converter.attributes)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>extension.add-attribute</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+
+
+                                                                        <v-accordion id="http-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                                                            <v-pane id="http-timeseries-pane_{{configIndex}}{{converterIndex}}">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.timeseries' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="converter.timeseries.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in converter.timeseries">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, converter.timeseries)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$invalid">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="httpTimeseriesKey_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
+                                                                                                                <div ng-messages="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$invalid">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="httpTimeseriesType_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$invalid">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="httpTimeseriesValue_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+                                                                                                                <div ng-messages="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+
+
+                                                                                                            <md-input-container flex="40" class="md-block">
+                                                                                                                <label translate>extension.transformer</label>
+                                                                                                                <md-select name="httpTimeseriesTransformer" ng-model="timeseries.transformerType" ng-change="transformerTypeChange(timeseries)">
+                                                                                                                    <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
+                                                                                                                        {{value | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+
+                                                                                                        <div ng-if='timeseries.transformerType == "custom"'>
+                                                                                                            <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+                                                                                                            <div flex class="tb-extension-custom-transformer-panel">
+                                                                                                                <div flex class="tb-extension-custom-transformer"
+                                                                                                                     ui-ace="extensionCustomTransformerOptions"
+                                                                                                                     ng-model="timeseries.transformer"
+                                                                                                                     name="timeseriesCustomTransformer_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}"
+                                                                                                                     ng-change='validateTransformer(timeseries.transformer,"timeseriesCustomTransformer_" + configIndex + converterIndex + timeseriesIndex)'
+                                                                                                                     required>
+                                                                                                                </div>
+                                                                                                            </div>
+                                                                                                            <div class="tb-error-messages" ng-messages="theForm['timeseriesCustomTransformer_' + configIndex + converterIndex + timeseriesIndex].$error" role="alert">
+                                                                                                                <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
+                                                                                                                <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
+                                                                                                            </div>
+                                                                                                        </div>
+
+
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>extension.add-timeseries</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click="addConverter(config.converters)" aria-label="{{ 'action.add' | translate }}">
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-converter</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                    </md-card-content>
+                                </md-card>
+                            </li>
+                        </ol>
+                    </div>
+                    <div flex layout="row" layout-align="start center">
+                        <md-button class="md-primary md-raised"
+                                   ng-click="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
+                            <md-icon class="material-icons">add</md-icon>
+                            <span translate>extension.add-config</span>
+                        </md-button>
+                    </div>
+                </v-pane-content>
+            </v-pane>
+        </v-accordion>
+        <!--{{config}}-->
+    </md-card-content>
+</md-card>
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
new file mode 100644
index 0000000..39a1eb8
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
@@ -0,0 +1,347 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
+
+    var linker = function(scope, element) {
+
+        var template = $templateCache.get(extensionFormMqttTemplate);
+        element.html(template);
+
+        scope.types = types;
+        scope.theForm = scope.$parent.theForm;
+
+        scope.deviceNameExpressions = {
+            deviceNameJsonExpression: "extension.converter-json",
+            deviceNameTopicExpression: "extension.topic"
+        };
+        scope.deviceTypeExpressions = {
+            deviceTypeJsonExpression: "extension.converter-json",
+            deviceTypeTopicExpression: "extension.topic"
+        };
+        scope.attributeKeyExpressions = {
+            attributeKeyJsonExpression: "extension.converter-json",
+            attributeKeyTopicExpression: "extension.topic"
+        };
+        scope.requestIdExpressions = {
+            requestIdJsonExpression: "extension.converter-json",
+            requestIdTopicExpression: "extension.topic"
+        }
+
+        scope.extensionCustomConverterOptions = {
+            useWrapMode: false,
+            mode: 'json',
+            showGutter: true,
+            showPrintMargin: true,
+            theme: 'github',
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function(_ace) {
+                _ace.$blockScrolling = 1;
+            }
+        };
+
+        scope.updateValidity = function () {
+            if(scope.brokers.length) {
+                for(let i=0;i<scope.brokers.length;i++) {
+                    if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
+                        if(!(scope.brokers[i].credentials.caCert && scope.brokers[i].credentials.privateKey && scope.brokers[i].credentials.cert)) {
+                            scope.theForm.$setValidity('cert.PEM', false);
+                            break;
+                        } else {
+                            scope.theForm.$setValidity('cert.PEM', true);
+                        }
+                    }
+                }
+            }
+        };
+
+        scope.$watch('brokers', function() {
+            scope.updateValidity();
+        }, true);
+
+        scope.addBroker = function() {
+            var newBroker = {
+                host: "localhost",
+                port: 1882,
+                ssl: false,
+                retryInterval: 3000,
+                credentials: {type:"anonymous"},
+                mapping: [],
+                connectRequests: [],
+                disconnectRequests: [],
+                attributeRequests: [],
+                attributeUpdates: [],
+                serverSideRpc: []
+            };
+            scope.brokers.push(newBroker);
+        };
+
+        scope.removeBroker = function(broker) {
+            var index = scope.brokers.indexOf(broker);
+            if (index > -1) {
+                scope.brokers.splice(index, 1);
+            }
+        };
+
+        if(scope.isAdd) {
+            scope.brokers = [];
+            scope.config.brokers = scope.brokers;
+            scope.addBroker();
+        } else {
+            scope.brokers = scope.config.brokers;
+        }
+
+        scope.addMap = function(mapping) {
+            var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}};
+
+            mapping.push(newMap);
+        };
+
+        scope.removeMap = function(map, mapping) {
+            var index = mapping.indexOf(map);
+            if (index > -1) {
+                mapping.splice(index, 1);
+            }
+        };
+
+        scope.addAttribute = function(attributes) {
+            var newAttribute = {type:"", key:"", value:""};
+            attributes.push(newAttribute);
+        };
+
+        scope.removeAttribute = function(attribute, attributes) {
+            var index = attributes.indexOf(attribute);
+            if (index > -1) {
+                attributes.splice(index, 1);
+            }
+        };
+
+        scope.addConnectRequest = function(requests, type) {
+            var newRequest = {};
+            if(type == "connect") {
+                newRequest.topicFilter = "sensors/connect";
+            } else {
+                newRequest.topicFilter = "sensors/disconnect";
+            }
+            requests.push(newRequest);
+        };
+
+        scope.addAttributeRequest = function(requests) {
+            var newRequest = {
+                topicFilter: "sensors/attributes",
+                clientScope: false,
+                responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}",
+                valueExpression: "${attributeValue}"
+            };
+            requests.push(newRequest);
+        };
+
+        scope.addAttributeUpdate = function(updates) {
+            var newUpdate = {
+                deviceNameFilter: ".*",
+                attributeFilter: ".*",
+                topicExpression: "sensor/${deviceName}/${attributeKey}",
+                valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}"
+            }
+            updates.push(newUpdate);
+        };
+
+        scope.addServerSideRpc = function(rpcRequests) {
+            var newRpc = {
+                deviceNameFilter: ".*",
+                methodFilter: "echo",
+                requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}",
+                responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}",
+                responseTimeout: 10000,
+                valueExpression: "${params}"
+            };
+            rpcRequests.push(newRpc);
+        };
+
+        scope.changeCredentials = function(broker) {
+            var type = broker.credentials.type;
+            broker.credentials = {};
+            broker.credentials.type = type;
+        };
+
+        scope.changeConverterType = function(map) {
+            if(map.converterType == "custom"){
+                map.converter = "";
+            }
+            if(map.converterType == "json") {
+                map.converter = {attributes:[],timeseries:[]};
+            }
+        };
+
+        scope.changeNameExpression = function(element, type) {
+            if(element.nameExp == "deviceNameJsonExpression") {
+                if(element.deviceNameTopicExpression) {
+                    delete element.deviceNameTopicExpression;
+                }
+                if(type) {
+                    element.deviceNameJsonExpression = "${$.serialNumber}";
+                }
+            }
+            if(element.nameExp == "deviceNameTopicExpression") {
+                if(element.deviceNameJsonExpression) {
+                    delete element.deviceNameJsonExpression;
+                }
+                if(type && type == "connect") {
+                    element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)";
+                }
+                if(type && type == "disconnect") {
+                    element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)";
+                }
+                if(type && type == "attribute") {
+                    element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)";
+                }
+            }
+        };
+
+        scope.changeTypeExpression = function(converter) {
+            if(converter.typeExp == "deviceTypeJsonExpression") {
+                if(converter.deviceTypeTopicExpression) {
+                    delete converter.deviceTypeTopicExpression;
+                }
+            }
+            if(converter.typeExp == "deviceTypeTopicExpression") {
+                if(converter.deviceTypeJsonExpression) {
+                    delete converter.deviceTypeJsonExpression;
+                }
+            }
+        };
+
+        scope.changeAttrKeyExpression = function(request) {
+            if(request.attrKey == "attributeKeyJsonExpression") {
+                if(request.attributeKeyTopicExpression) {
+                    delete request.attributeKeyTopicExpression;
+                }
+                request.attributeKeyJsonExpression = "${$.key}";
+            }
+            if(request.attrKey == "attributeKeyTopicExpression") {
+                if(request.attributeKeyJsonExpression) {
+                    delete request.attributeKeyJsonExpression;
+                }
+                request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)";
+            }
+        };
+
+        scope.changeRequestIdExpression = function(request) {
+            if(request.requestId == "requestIdJsonExpression") {
+                if(request.requestIdTopicExpression) {
+                    delete request.requestIdTopicExpression;
+                }
+                request.requestIdJsonExpression = "${$.requestId}";
+            }
+            if(request.requestId == "requestIdTopicExpression") {
+                if(request.requestIdJsonExpression) {
+                    delete request.requestIdJsonExpression;
+                }
+                request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)";
+            }
+        };
+
+        scope.validateCustomConverter = function(model, editorName) {
+            if(model && model.length) {
+                try {
+                    angular.fromJson(model);
+                    scope.theForm[editorName].$setValidity('converterJSON', true);
+                } catch(e) {
+                    scope.theForm[editorName].$setValidity('converterJSON', false);
+                }
+            }
+        };
+
+        scope.fileAdded = function($file, broker, fileType) {
+            var reader = new FileReader();
+            reader.onload = function(event) {
+                scope.$apply(function() {
+                    if(event.target.result) {
+                        scope.theForm.$setDirty();
+                        var addedFile = event.target.result;
+                        if (addedFile && addedFile.length > 0) {
+                            if(fileType == "caCert") {
+                                broker.credentials.caCertFileName = $file.name;
+                                broker.credentials.caCert = addedFile.replace(/^data.*base64,/, "");
+                            }
+                            if(fileType == "privateKey") {
+                                broker.credentials.privateKeyFileName = $file.name;
+                                broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, "");
+                            }
+                            if(fileType == "Cert") {
+                                broker.credentials.certFileName = $file.name;
+                                broker.credentials.cert = addedFile.replace(/^data.*base64,/, "");
+                            }
+                        }
+                    }
+                });
+            };
+            reader.readAsDataURL($file.file);
+        };
+
+        scope.clearFile = function(broker, fileType) {
+            scope.theForm.$setDirty();
+            if(fileType == "caCert") {
+                broker.credentials.caCertFileName = null;
+                broker.credentials.caCert = null;
+            }
+            if(fileType == "privateKey") {
+                broker.credentials.privateKeyFileName = null;
+                broker.credentials.privateKey = null;
+            }
+            if(fileType == "Cert") {
+                broker.credentials.certFileName = null;
+                broker.credentials.cert = null;
+            }
+        };
+
+        scope.collapseValidation = function(index, id) {
+            var invalidState = angular.element('#'+id+':has(.ng-invalid)');
+            if(invalidState.length) {
+                invalidState.addClass('inner-invalid');
+            }
+        };
+
+        scope.expandValidation = function (index, id) {
+            var invalidState = angular.element('#'+id);
+            invalidState.removeClass('inner-invalid');
+        };
+
+        $compile(element.contents())(scope);
+    };
+
+    return {
+        restrict: "A",
+        link: linker,
+        scope: {
+            config: "=",
+            isAdd: "="
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
new file mode 100644
index 0000000..f665fb0
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
@@ -0,0 +1,860 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-card class="extension-form extension-mqtt">
+    <md-card-title name="testValid">
+        <md-card-title-text>
+            <span translate class="md-headline">extension.configuration</span>
+        </md-card-title-text>
+    </md-card-title>
+    <md-card-content>
+        <v-accordion id="mqtt-brokers-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+            <v-pane id="mqtt-brokers-pane" expanded="true">
+                <v-pane-header>
+                    {{ 'extension.brokers' | translate }}
+                </v-pane-header>
+                <v-pane-content>
+                    <div ng-if="brokers.length > 0">
+                        <ol class="list-group">
+                            <li class="list-group-item" ng-repeat="(brokerIndex,broker) in brokers">
+                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeBroker(broker)" ng-hide="brokers.length < 2">
+                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.remove' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+                                <md-card>
+                                    <md-card-content>
+                                        <section flex layout="row">
+                                            <md-input-container flex="40" class="md-block">
+                                                <label translate>extension.port</label>
+                                                <input required type="number" min="1" max="65535" name="mqttPort_{{brokerIndex}}" ng-model="broker.port">
+                                                <div ng-messages="theForm['mqttPort_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                    <div translate ng-message="min">extension.port-range</div>
+                                                    <div translate ng-message="max">extension.port-range</div>
+                                                </div>
+                                            </md-input-container>
+                                            <md-input-container flex="60" class="md-block">
+                                                <label translate>extension.host</label>
+                                                <input required name="mqttHost_{{brokerIndex}}" ng-model="broker.host">
+                                                <div ng-messages="theForm['mqttHost_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </section>
+                                        <section flex layout="row">
+                                            <md-input-container flex="40" class="md-block">
+                                                <label translate>extension.retry-interval</label>
+                                                <input required type="number" name="mqttRetryInterval_{{brokerIndex}}" ng-model="broker.retryInterval">
+                                                <div ng-messages="theForm['mqttRetryInterval_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.credentials</label>
+                                                <md-select required name="mqttCredentials_{{brokerIndex}}" ng-model="broker.credentials.type" ng-change="changeCredentials(broker)">
+                                                    <md-option ng-repeat="(credentialsType, credentialsValue) in types.mqttCredentialTypes" ng-value="credentialsValue.value">
+                                                        {{credentialsValue.name | translate}}
+                                                    </md-option>
+                                                </md-select>
+                                            </md-input-container>
+                                            <md-input-container flex="10" class="md-block">
+                                                <md-checkbox flex aria-label="{{ 'extension.ssl' | translate }}"
+                                                             ng-model="broker.ssl">{{ 'extension.ssl' | translate }}
+                                                </md-checkbox>
+                                            </md-input-container>
+                                        </section>
+                                        <section flex layout="row" ng-if='broker.credentials.type == "basic"'>
+                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttUsername_' + brokerIndex].$touched && theForm['mqttUsername_' + brokerIndex].$invalid">
+                                                <label translate>extension.username</label>
+                                                <input required name="mqttUsername_{{brokerIndex}}" ng-model="broker.credentials.username">
+                                                <div ng-messages="theForm['mqttUsername_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                            <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttPassword_' + brokerIndex].$touched && theForm['mqttPassword_' + brokerIndex].$invalid">
+                                                <label translate>extension.password</label>
+                                                <input required name="mqttPassword_{{brokerIndex}}" ng-model="broker.credentials.password">
+                                                <div ng-messages="theForm['mqttPassword_' + brokerIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </section>
+                                        <section flex layout="column" ng-if='broker.credentials.type == "cert.PEM"' class="dropdown-section">
+                                            <div class="tb-container" ng-class="broker.credentials.caCertFileName ? 'ng-valid' : 'ng-invalid'">
+                                                <label class="tb-label" translate>extension.ca-cert</label>
+                                                <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "caCert")' class="tb-file-select-container">
+                                                    <div class="tb-file-clear-container">
+                                                        <md-button ng-click='clearFile(broker, "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_{{brokerIndex}}" translate>extension.drop-file</label>
+                                                        <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="caCertSelect_{{brokerIndex}}">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="dropdown-messages">
+                                                <div ng-if="!broker.credentials.caCertFileName" class="tb-error-message" translate>extension.no-file</div>
+                                                <div ng-if="broker.credentials.caCertFileName">{{broker.credentials.caCertFileName}}</div>
+                                            </div>
+                                            <div class="tb-container" ng-class="broker.credentials.privateKeyFileName ? 'ng-valid' : 'ng-invalid'">
+                                                <label class="tb-label" translate>extension.private-key</label>
+                                                <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "privateKey")' class="tb-file-select-container">
+                                                    <div class="tb-file-clear-container">
+                                                        <md-button ng-click='clearFile(broker, "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_{{brokerIndex}}" translate>extension.drop-file</label>
+                                                        <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="privateKeySelect_{{brokerIndex}}">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="dropdown-messages">
+                                                <div ng-if="!broker.credentials.privateKeyFileName" class="tb-error-message" translate>extension.no-file</div>
+                                                <div ng-if="broker.credentials.privateKeyFileName">{{broker.credentials.privateKeyFileName}}</div>
+                                            </div>
+                                            <div class="tb-container" ng-class="broker.credentials.certFileName ? 'ng-valid' : 'ng-invalid'">
+                                                <label class="tb-label" translate>extension.cert</label>
+                                                <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "Cert")' class="tb-file-select-container">
+                                                    <div class="tb-file-clear-container">
+                                                        <md-button ng-click='clearFile(broker, "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_{{brokerIndex}}" translate>extension.drop-file</label>
+                                                        <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="CertSelect_{{brokerIndex}}">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                            <div class="dropdown-messages">
+                                                <div ng-if="!broker.credentials.certFileName" class="tb-error-message" translate>extension.no-file</div>
+                                                <div ng-if="broker.credentials.certFileName">{{broker.credentials.certFileName}}</div>
+                                            </div>
+                                        </section>
+
+                                        <v-accordion id="mqtt-mapping-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="mqtt-mapping-pane_{{brokerIndex}}">
+                                                <v-pane-header>
+                                                    {{ 'extension.mapping' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="broker.mapping.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item" ng-repeat="(mapIndex,map) in broker.mapping">
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeMap(map, broker.mapping)">
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttConverterType_' + brokerIndex + mapIndex].$touched && theForm['mqttConverterType_' + brokerIndex + mapIndex].$invalid">
+                                                                                <label translate>extension.converter-type</label>
+                                                                                <md-select required name="mqttConverterType_{{brokerIndex}}{{mapIndex}}" ng-model="map.converterType" ng-change="changeConverterType(map)">
+                                                                                    <md-option ng-repeat="(converterType, value) in types.mqttConverterTypes" ng-value="converterType">
+                                                                                        {{value | translate}}
+                                                                                    </md-option>
+                                                                                </md-select>
+                                                                                <div ng-messages="theForm['mqttConverterType_' + brokerIndex + mapIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container flex="60" class="md-block">
+                                                                                <label translate>extension.topic-filter</label>
+                                                                                <input required name="mqttTopicFilter_{{brokerIndex}}{{mapIndex}}" ng-model="map.topicFilter">
+                                                                                <div ng-messages="theForm['mqttTopicFilter_' + brokerIndex + mapIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+
+                                                                        <div ng-if='map.converterType =="json"' ng-init="map.converter.type = 'json'">
+                                                                            <section flex layout="row">
+                                                                                <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$invalid">
+                                                                                    <label translate>extension.device-name-expression</label>
+                                                                                    <md-select required name="mqttDeviceNameExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.nameExp" ng-change="changeNameExpression(map.converter)">
+                                                                                        <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+                                                                                            {{value | translate}}
+                                                                                        </md-option>
+                                                                                    </md-select>
+                                                                                    <div ng-messages="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.field-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$invalid">
+                                                                                    <label translate>extension.json-name-expression</label>
+                                                                                    <input required name="mqttJsonNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameJsonExpression">
+                                                                                    <div ng-messages="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.field-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$invalid">
+                                                                                    <label translate>extension.topic-name-expression</label>
+                                                                                    <input required name="mqttTopicNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameTopicExpression">
+                                                                                    <div ng-messages="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.field-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                            </section>
+                                                                            <section flex layout="row">
+                                                                                <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$invalid">
+                                                                                    <label translate>extension.device-type-expression</label>
+                                                                                    <md-select required name="mqttDeviceTypeExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.typeExp" ng-change="changeTypeExpression(map.converter)">
+                                                                                        <md-option ng-repeat="(key, value) in deviceTypeExpressions" ng-value='key'>
+                                                                                            {{value | translate}}
+                                                                                        </md-option>
+                                                                                    </md-select>
+                                                                                    <div ng-messages="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.field-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.typeExp == 'deviceTypeJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$invalid">
+                                                                                    <label translate>extension.json-type-expression</label>
+                                                                                    <input required name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
+                                                                                    <div ng-messages="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.field-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                                <md-input-container ng-if="map.converter.typeExp == 'deviceTypeTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$invalid">
+                                                                                    <label translate>extension.topic-type-expression</label>
+                                                                                    <input required name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
+                                                                                    <div ng-messages="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.field-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                            </section>
+                                                                            <section flex layout="row">
+                                                                                <md-input-container flex="40" class="md-block">
+                                                                                    <label translate>extension.timeout</label>
+                                                                                    <input type="number" name="mqttTimeout_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.timeout" parse-to-null>
+                                                                                </md-input-container>
+                                                                                <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
+                                                                                    <label translate>extension.filter-expression</label>
+                                                                                    <input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
+                                                                                    <div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
+                                                                                        <div translate ng-message="required">extension.field-required</div>
+                                                                                    </div>
+                                                                                </md-input-container>
+                                                                            </section>
+                                                                        </div>
+
+                                                                        <div ng-if='map.converterType == "custom"'>
+                                                                            <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+                                                                            <div flex class="tb-extension-custom-transformer-panel">
+                                                                                <div flex class="tb-extension-custom-transformer"
+                                                                                     ui-ace="extensionCustomConverterOptions"
+                                                                                     ng-model="map.converter"
+                                                                                     name="mqttCustomConverter_{{brokerIndex}}{{mapIndex}}"
+                                                                                     ng-change='validateCustomConverter(map.converter, "mqttCustomConverter_" + brokerIndex + mapIndex)'
+                                                                                     required>
+                                                                                </div>
+                                                                            </div>
+                                                                            <div class="tb-error-messages" ng-messages="theForm['mqttCustomConverter_' + brokerIndex + mapIndex].$error" role="alert">
+                                                                                <div ng-message="required" class="tb-error-message" translate>extension.converter-json-required</div>
+                                                                                <div ng-message="converterJSON" class="tb-error-message" translate>extension.converter-json-parse</div>
+                                                                            </div>
+                                                                        </div>
+
+                                                                        <v-accordion ng-if='map.converterType =="json"' id="mqtt-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                                                            <v-pane id="mqtt-attributes-pane_{{brokerIndex}}{{mapIndex}}">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.attributes' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="map.converter.attributes.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in map.converter.attributes">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, map.converter.attributes)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$invalid">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="mqttAttributeKey_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.key">
+                                                                                                                <div ng-messages="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$invalid">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="mqttAttributeType_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <md-input-container class="md-block" md-is-error="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$invalid">
+                                                                                                            <label translate>extension.value</label>
+                                                                                                            <input required name="mqttAttributeValue_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.value">
+                                                                                                            <div ng-messages="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$error">
+                                                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                                                            </div>
+                                                                                                        </md-input-container>
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(map.converter.attributes)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>extension.add-attribute</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+
+                                                                        <v-accordion ng-if='map.converterType =="json"' id="mqtt-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                                                            <v-pane id="mqtt-timeseries-pane_{{brokerIndex}}{{mapIndex}}">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.timeseries' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-if="map.converter.timeseries.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in map.converter.timeseries">
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, map.converter.timeseries)">
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required name="mqttTimeseriesKey_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
+                                                                                                                <div ng-messages="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="mqttTimeseriesType_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <md-input-container class="md-block" md-is-error="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
+                                                                                                            <label translate>extension.value</label>
+                                                                                                            <input required name="mqttTimeseriesValue_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+                                                                                                            <div ng-messages="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                                                            </div>
+                                                                                                        </md-input-container>
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addAttribute(map.converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>extension.add-timeseries</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click="addMap(broker.mapping)" aria-label="{{ 'action.add' | translate }}">
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-map</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                        <v-accordion id="mqtt-connect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="mqtt-connect-requests-pane_{{brokerIndex}}">
+                                                <v-pane-header>
+                                                    {{ 'extension.connect-requests' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="broker.connectRequests.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item" ng-repeat="(connectRequestIndex, connectRequest) in broker.connectRequests">
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(connectRequest, broker.connectRequests)">
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.topic-filter</label>
+                                                                            <input required name="conRequestTopicFilter_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.topicFilter">
+                                                                            <div ng-messages="theForm['conRequestTopicFilter_' + brokerIndex + connectRequestIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$touched && theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$invalid">
+                                                                                <label translate>extension.device-name-expression</label>
+                                                                                <md-select required name="connectDeviceNameExpression_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.nameExp" ng-change="changeNameExpression(connectRequest, 'connect')">
+                                                                                    <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+                                                                                        {{value | translate}}
+                                                                                    </md-option>
+                                                                                </md-select>
+                                                                                <div ng-messages="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="connectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.json-name-expression</label>
+                                                                                <input required name="connectJsonNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameJsonExpression">
+                                                                                <div ng-messages="theForm['connectJsonNameExp_' + brokerIndex + connectRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="connectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.topic-name-expression</label>
+                                                                                <input required name="connectTopicNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameTopicExpression">
+                                                                                <div ng-messages="theForm['connectTopicNameExp_' + brokerIndex + connectRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click='addConnectRequest(broker.connectRequests, "connect")' aria-label="{{ 'action.add' | translate }}">
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-connect-request</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                        <v-accordion id="mqtt-disconnect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="mqtt-disconnect-requests-pane_{{brokerIndex}}">
+                                                <v-pane-header>
+                                                    {{ 'extension.disconnect-requests' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="broker.disconnectRequests.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item" ng-repeat="(disconnectRequestIndex, disconnectRequest) in broker.disconnectRequests">
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(disconnectRequest, broker.disconnectRequests)">
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.topic-filter</label>
+                                                                            <input required name="disconRequestTopicFilter_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.topicFilter">
+                                                                            <div ng-messages="theForm['disconRequestTopicFilter_' + brokerIndex + disconnectRequestIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$touched && theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$invalid">
+                                                                                <label translate>extension.device-name-expression</label>
+                                                                                <md-select required name="disconnectDeviceNameExpression_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.nameExp" ng-change="changeNameExpression(disconnectRequest, 'disconnect')">
+                                                                                    <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+                                                                                        {{value | translate}}
+                                                                                    </md-option>
+                                                                                </md-select>
+                                                                                <div ng-messages="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.json-name-expression</label>
+                                                                                <input required name="disconnectJsonNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameJsonExpression">
+                                                                                <div ng-messages="theForm['disconnectJsonNameExp_' + brokerIndex + disconnectRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.topic-name-expression</label>
+                                                                                <input required name="disconnectTopicNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameTopicExpression">
+                                                                                <div ng-messages="theForm['disconnectTopicNameExp_' + brokerIndex + disconnectRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click='addConnectRequest(broker.disconnectRequests, "disconnect")' aria-label="{{ 'action.add' | translate }}">
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-disconnect-request</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                        <v-accordion id="mqtt-attribute-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="mqtt-attribute-requests-pane_{{brokerIndex}}">
+                                                <v-pane-header>
+                                                    {{ 'extension.attribute-requests' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="broker.attributeRequests.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item" ng-repeat="(attributeRequestIndex, attributeRequest) in broker.attributeRequests">
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeRequest, broker.attributeRequests)">
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="80" class="md-block">
+                                                                                <label translate>extension.topic-filter</label>
+                                                                                <input required name="attributeRequestTopicFilter_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.topicFilter">
+                                                                                <div ng-messages="theForm['attributeRequestTopicFilter_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container flex="20" class="md-block">
+                                                                                <md-checkbox flex aria-label="{{ 'extension.client-scope' | translate }}"
+                                                                                             ng-model="attributeRequest.clientScope">{{ 'extension.client-scope' | translate }}
+                                                                                </md-checkbox>
+                                                                            </md-input-container>
+                                                                        </section>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$invalid">
+                                                                                <label translate>extension.device-name-expression</label>
+                                                                                <md-select required name="attrRequestDeviceNameExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.nameExp" ng-change="changeNameExpression(attributeRequest, 'attribute')">
+                                                                                    <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+                                                                                        {{value | translate}}
+                                                                                    </md-option>
+                                                                                </md-select>
+                                                                                <div ng-messages="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="attributeRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.json-name-expression</label>
+                                                                                <input required name="attrRequestJsonNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameJsonExpression">
+                                                                                <div ng-messages="theForm['attrRequestJsonNameExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="attributeRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.topic-name-expression</label>
+                                                                                <input required name="attrRequestTopicNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameTopicExpression">
+                                                                                <div ng-messages="theForm['attrRequestTopicNameExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$invalid">
+                                                                                <label translate>extension.attribute-key-expression</label>
+                                                                                <md-select required name="attrRequestAttributeKeyExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attrKey" ng-change="changeAttrKeyExpression(attributeRequest)">
+                                                                                    <md-option ng-repeat="(key, value) in attributeKeyExpressions" ng-value='key'>
+                                                                                        {{value | translate}}
+                                                                                    </md-option>
+                                                                                </md-select>
+                                                                                <div ng-messages="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyJsonExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.attr-json-key-expression</label>
+                                                                                <input required name="attrRequestJsonKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyJsonExpression">
+                                                                                <div ng-messages="theForm['attrRequestJsonKeyExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyTopicExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.attr-topic-key-expression</label>
+                                                                                <input required name="attrRequestTopicKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyTopicExpression">
+                                                                                <div ng-messages="theForm['attrRequestTopicKeyExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$invalid">
+                                                                                <label translate>extension.request-id-expression</label>
+                                                                                <md-select required name="attrRequestIdExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestId" ng-change="changeRequestIdExpression(attributeRequest)">
+                                                                                    <md-option ng-repeat="(key, value) in requestIdExpressions" ng-value='key'>
+                                                                                        {{value | translate}}
+                                                                                    </md-option>
+                                                                                </md-select>
+                                                                                <div ng-messages="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="attributeRequest.requestId == 'requestIdJsonExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.request-id-json-expression</label>
+                                                                                <input required name="attrRequestJsonIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdJsonExpression">
+                                                                                <div ng-messages="theForm['attrRequestJsonIdExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container ng-if="attributeRequest.requestId == 'requestIdTopicExpression'" flex="60" class="md-block">
+                                                                                <label translate>extension.request-id-topic-expression</label>
+                                                                                <input required name="attrRequestTopicIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdTopicExpression">
+                                                                                <div ng-messages="theForm['attrRequestTopicIdExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.response-topic-expression</label>
+                                                                            <input required name="attributeRequestResponseTopicExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.responseTopicExpression">
+                                                                            <div ng-messages="theForm['attributeRequestResponseTopicExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.value-expression</label>
+                                                                            <input required name="attributeRequestValueExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.valueExpression">
+                                                                            <div ng-messages="theForm['attributeRequestValueExp_' + brokerIndex + attributeRequestIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click="addAttributeRequest(broker.attributeRequests)" aria-label="{{ 'action.add' | translate }}">
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-attribute-request</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                        <v-accordion id="mqtt-attribute-updates-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="mqtt-attribute-updates-pane_{{brokerIndex}}">
+                                                <v-pane-header>
+                                                    {{ 'extension.attribute-updates' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="broker.attributeUpdates.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item" ng-repeat="(attributeUpdateIndex, attributeUpdate) in broker.attributeUpdates">
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeUpdate, broker.attributeUpdates)">
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.device-name-filter</label>
+                                                                                <input required name="attributeUpdateDeviceNameFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.deviceNameFilter">
+                                                                                <div ng-messages="theForm['attributeUpdateDeviceNameFilter_' + brokerIndex + attributeUpdateIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.attribute-filter</label>
+                                                                                <input required name="attributeUpdateAttributeFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.attributeFilter">
+                                                                                <div ng-messages="theForm['attributeUpdateAttributeFilter_' + brokerIndex + attributeUpdateIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.topic-expression</label>
+                                                                            <input required name="attributeUpdateTopicExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.topicExpression">
+                                                                            <div ng-messages="theForm['attributeUpdateTopicExp_' + brokerIndex + attributeUpdateIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.value-expression</label>
+                                                                            <input required name="attributeUpdateValueExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.valueExpression">
+                                                                            <div ng-messages="theForm['attributeUpdateValueExp_' + brokerIndex + attributeUpdateIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click='addAttributeUpdate(broker.attributeUpdates)' aria-label="{{ 'action.add' | translate }}">
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-attribute-update</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                        <v-accordion id="mqtt-server-side-rpc-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="mqtt-server-side-rpc-pane_{{brokerIndex}}">
+                                                <v-pane-header>
+                                                    {{ 'extension.server-side-rpc' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="broker.serverSideRpc.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item" ng-repeat="(rpcIndex, rpc) in broker.serverSideRpc">
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(rpc, broker.serverSideRpc)">
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.device-name-filter</label>
+                                                                                <input required name="serverSideRpcDeviceNameFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.deviceNameFilter">
+                                                                                <div ng-messages="theForm['serverSideRpcDeviceNameFilter_' + brokerIndex + rpcIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.method-filter</label>
+                                                                                <input required name="serverSideRpcMethodFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.methodFilter">
+                                                                                <div ng-messages="theForm['serverSideRpcMethodFilter_' + brokerIndex + rpcIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.request-topic-expression</label>
+                                                                            <input required name="serverSideRpcRequestTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.requestTopicExpression">
+                                                                            <div ng-messages="theForm['serverSideRpcRequestTopicExp_' + brokerIndex + rpcIndex].$error">
+                                                                                <div translate ng-message="required">extension.field-required</div>
+                                                                            </div>
+                                                                        </md-input-container>
+                                                                        <md-input-container class="md-block">
+                                                                            <label translate>extension.response-topic-expression</label>
+                                                                            <input name="serverSideRpcResponseTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTopicExpression" parse-to-null>
+                                                                        </md-input-container>
+                                                                        <section flex layout="row">
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.response-timeout</label>
+                                                                                <input type="number" name="serverSideRpcResponseTimeout_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTimeout" parse-to-null>
+                                                                            </md-input-container>
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.value-expression</label>
+                                                                                <input required name="serverSideRpcValueExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.valueExpression">
+                                                                                <div ng-messages="theForm['serverSideRpcValueExp_' + brokerIndex + rpcIndex].$error">
+                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </section>
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex layout="row" layout-align="start center">
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click='addServerSideRpc(broker.serverSideRpc)' aria-label="{{ 'action.add' | translate }}">
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-server-side-rpc-request</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                    </md-card-content>
+                                </md-card>
+                            </li>
+                        </ol>
+                    </div>
+
+                    <div flex layout="row" layout-align="start center">
+                        <md-button class="md-primary md-raised"
+                                   ng-click="addBroker()" aria-label="{{ 'action.add' | translate }}">
+                            <md-icon class="material-icons">add</md-icon>
+                            <span translate>extension.add-broker</span>
+                        </md-button>
+                    </div>
+                </v-pane-content>
+            </v-pane>
+        </v-accordion>
+<!--<pre>
+{{config | json}}
+</pre>-->
+    </md-card-content>
+</md-card>
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
new file mode 100644
index 0000000..fc69b68
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
@@ -0,0 +1,173 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/theme/github';
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormOpcTemplate from './extension-form-opc.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) {
+
+
+    var linker = function(scope, element) {
+
+
+        function Server() {
+            this.applicationName = "Thingsboard OPC-UA client";
+            this.applicationUri = "";
+            this.host = "localhost";
+            this.port = 49320;
+            this.scanPeriodInSeconds = 10;
+            this.timeoutInMillis = 5000;
+            this.security = "Basic128Rsa15";
+            this.identity = {
+                "type": "anonymous"
+            };
+            this.keystore = {
+                "type": "PKCS12",
+                "location": "example.pfx",
+                "password": "secret",
+                "alias": "gateway",
+                "keyPassword": "secret"
+            };
+            this.mapping = []
+        }
+
+        function Map() {
+            this.deviceNodePattern = "Channel1\\.Device\\d+$";
+            this.deviceNamePattern = "Device ${_System._DeviceId}";
+            this.attributes = [];
+            this.timeseries = [];
+        }
+
+        function Attribute() {
+            this.key = "Tag1";
+            this.type = "string";
+            this.value = "${Tag1}";
+        }
+
+        function Timeseries() {
+            this.key = "Tag2";
+            this.type = "long";
+            this.value = "${Tag2}";
+        }
+
+
+        var template = $templateCache.get(extensionFormOpcTemplate);
+        element.html(template);
+
+        scope.types = types;
+        scope.theForm = scope.$parent.theForm;
+
+
+        if (!scope.configuration.servers.length) {
+            scope.configuration.servers.push(new Server());
+        }
+
+        scope.addServer = function(serversList) {
+            serversList.push(new Server());
+            // scope.addMap(serversList[serversList.length-1].mapping);
+
+            scope.theForm.$setDirty();
+        };
+
+        scope.addMap = function(mappingList) {
+            mappingList.push(new Map());
+            scope.theForm.$setDirty();
+        };
+
+        scope.addNewAttribute = function(attributesList) {
+            attributesList.push(new Attribute());
+            scope.theForm.$setDirty();
+        };
+
+        scope.addNewTimeseries = function(timeseriesList) {
+            timeseriesList.push(new Timeseries());
+            scope.theForm.$setDirty();
+        };
+
+
+        scope.removeItem = (item, itemList) => {
+            var index = itemList.indexOf(item);
+            if (index > -1) {
+                itemList.splice(index, 1);
+            }
+            scope.theForm.$setDirty();
+        };
+
+
+        $compile(element.contents())(scope);
+
+
+        scope.fileAdded = function($file, model, options) {
+            let reader = new FileReader();
+            reader.onload = function(event) {
+                scope.$apply(function() {
+                    if(event.target.result) {
+                        scope.theForm.$setDirty();
+                        let addedFile = event.target.result;
+
+                        if (addedFile && addedFile.length > 0) {
+                            model[options.fileName] = $file.name;
+                            model[options.file] = addedFile.replace(/^data.*base64,/, "");
+
+                        }
+                    }
+                });
+            };
+            reader.readAsDataURL($file.file);
+
+        };
+
+        scope.clearFile = function(model, options) {
+            scope.theForm.$setDirty();
+
+            model[options.fileName] = null;
+            model[options.file] = null;
+
+        };
+
+        scope.collapseValidation = function(index, id) {
+            var invalidState = angular.element('#'+id+':has(.ng-invalid)');
+            if(invalidState.length) {
+                invalidState.addClass('inner-invalid');
+            }
+        };
+
+        scope.expandValidation = function (index, id) {
+            var invalidState = angular.element('#'+id);
+            invalidState.removeClass('inner-invalid');
+        };
+
+    };
+
+    return {
+        restrict: "A",
+        link: linker,
+        scope: {
+            configuration: "=",
+            isAdd: "="
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
new file mode 100644
index 0000000..6d74cc5
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
@@ -0,0 +1,557 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-card class="extension-form extension-opc">
+    <md-card-title>
+        <md-card-title-text>
+            <span translate class="md-headline">extension.configuration</span>
+        </md-card-title-text>
+    </md-card-title>
+
+    <md-card-content>
+        <v-accordion id="opc-server-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+            <v-pane id="opc-servers-pane" expanded="true">
+                <v-pane-header>
+                    {{ 'extension.opc-server' | translate }}
+                </v-pane-header>
+
+                <v-pane-content>
+                    <div ng-if="configuration.servers.length === 0">
+                        <span translate layout-align="center center" class="tb-prompt">extension.opc-add-server-prompt</span>
+                    </div>
+
+                    <div ng-if="configuration.servers.length > 0">
+                        <ol class="list-group">
+                            <li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
+                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                           class="md-icon-button"
+                                           ng-click="removeItem(server, configuration.servers)"
+                                           ng-hide="configuration.servers.length < 2"
+                                >
+                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.remove' | translate }}
+                                    </md-tooltip>
+                                </md-button>
+
+                                <md-card>
+                                    <md-card-content>
+
+                                        <div layout="row">
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-application-name</label>
+                                                <input required name="applicationName_{{serverIndex}}" ng-model="server.applicationName">
+                                                <div ng-messages="theForm['applicationName_' + serverIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+
+                                            <md-input-container flex="50" class="md-block" md-is-error="theForm['applicationUri_' + serverIndex].$touched && theForm['applicationUri_' + serverIndex].$invalid">
+                                                <label translate>extension.opc-application-uri</label>
+                                                <input required name="applicationUri_{{serverIndex}}" ng-model="server.applicationUri">
+                                                <div ng-messages="theForm['applicationUri_' + serverIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+
+                                        <div layout="row">
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.host</label>
+                                                <input required name="host_{{serverIndex}}" ng-model="server.host">
+                                                <div ng-messages="theForm['host_' + serverIndex].$error">
+                                                    <div translate ng-message="required">extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.port</label>
+                                                <input type="number"
+                                                       required
+                                                       name="port_{{serverIndex}}"
+                                                       ng-model="server.port"
+                                                       min="1"
+                                                       max="65535"
+                                                >
+                                                <div ng-messages="theForm['port_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.field-required</div>
+                                                    <div translate
+                                                         ng-message="min"
+                                                    >extension.port-range</div>
+                                                    <div translate
+                                                         ng-message="max"
+                                                    >extension.port-range</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+                                        <div layout="row">
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.opc-scan-period-in-seconds</label>
+                                                <input type="number"
+                                                       required
+                                                       name="scanPeriodInSeconds_{{serverIndex}}"
+                                                       ng-model="server.scanPeriodInSeconds">
+                                                <div ng-messages="theForm['scanPeriodInSeconds_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block">
+                                                <label translate>extension.timeout</label>
+                                                <input type="number"
+                                                       required name="timeoutInMillis_{{serverIndex}}"
+                                                       ng-model="server.timeoutInMillis"
+                                                >
+                                                <div ng-messages="theForm['timeoutInMillis_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+                                        <div layout="row">
+
+                                            <md-input-container flex="50" class="md-block tb-container-for-select">
+                                                <label translate>extension.opc-security</label>
+                                                <md-select required
+                                                           name="securityType_{{serverIndex}}"
+                                                           ng-model="server.security">
+                                                    <md-option ng-value="securityType"
+                                                               ng-repeat="(securityType, securityValue) in types.extensionOpcSecurityTypes"
+                                                    ><span ng-bind="::securityValue"></span></md-option>
+                                                </md-select>
+                                                <div ng-messages="theForm['securityType_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block tb-container-for-select">
+                                                <label translate>extension.opc-identity</label>
+                                                <md-select required
+                                                           name="identityType_{{serverIndex}}"
+                                                           ng-model="server.identity.type"
+                                                >
+                                                    <md-option ng-value="identityType"
+                                                               ng-repeat="(identityType, identityValue) in types.extensionIdentityType"
+                                                    ><span ng-bind="identityValue | translate"></span></md-option>
+                                                </md-select>
+                                                <div ng-messages="theForm['identityType_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+                                        <div ng-if="server.identity.type != 'username'">
+                                            <span class=""
+                                                  ng-init="server.identity = {'type':'anonymous'}"></span>
+                                        </div>
+                                        <div layout="row" ng-if="server.identity.type == 'username'">
+                                            <md-input-container flex="50" class="md-block" md-is-error="theForm['identityUsername_' + serverIndex].$touched && theForm['identityUsername_' + serverIndex].$invalid">
+                                                <label translate>extension.username</label>
+                                                <input required
+                                                       name="identityUsername_{{serverIndex}}"
+                                                       ng-model="server.identity.username"
+                                                >
+                                                <div ng-messages="theForm['identityUsername_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+
+                                            <md-input-container flex="50" class="md-block" md-is-error="theForm['identityPassword_' + serverIndex].$touched && theForm['identityPassword_' + serverIndex].$invalid">
+                                                <label translate>extension.password</label>
+                                                <input required
+                                                       name="identityPassword_{{serverIndex}}" ng-model="server.identity.password">
+                                                <div ng-messages="theForm['identityPassword_' + serverIndex].$error">
+                                                    <div translate
+                                                         ng-message="required"
+                                                    >extension.field-required</div>
+                                                </div>
+                                            </md-input-container>
+                                        </div>
+
+                                        <v-accordion id="opc-keystore-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true">
+                                                <v-pane-header>
+                                                    {{ 'extension.opc-keystore' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+
+                                                    <md-input-container class="md-block tb-container-for-select">
+                                                        <label translate>extension.opc-keystore-type</label>
+                                                        <md-select required name="keystoreType_{{serverIndex}}" ng-model="server.keystore.type">
+                                                            <md-option ng-value="keystoreType" ng-repeat="(keystoreType, keystoreValue) in types.extensionKeystoreType"><span ng-bind="::keystoreValue"></span></md-option>
+                                                        </md-select>
+                                                        <div ng-messages="theForm['keystoreType_'+serverIndex].$error">
+                                                            <div translate ng-message="required">extension.field-required</div>
+                                                        </div>
+                                                    </md-input-container>
+
+                                                    <section class="dropdown-section">
+                                                        <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
+                                                            <span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
+                                                            <label class="tb-label" translate>extension.opc-keystore-location</label>
+                                                            <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
+                                                                <div class="tb-file-clear-container">
+                                                                    <md-button ng-click='clearFile(server.keystore, fieldsToFill)' 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="dropFileKeystore_{{serverIndex}}" translate>extension.drop-file</label>
+                                                                    <input flow-attrs="{accept:'.pfx,.p12'}"
+                                                                           type="file"
+                                                                           class="file-input"
+                                                                           flow-btn id="dropFileKeystore_{{serverIndex}}"
+                                                                           name="keystoreFile"
+                                                                           ng-model="server.keystore.file"
+                                                                    >
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                        <div class="dropdown-messages">
+                                                            <div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
+                                                            <div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
+                                                        </div>
+                                                    </section>
+
+
+                                                    <div flex layout="row">
+                                                        <md-input-container flex="50" class="md-block">
+                                                            <label translate>extension.opc-keystore-password</label>
+                                                            <input required name="keystorePassword_{{serverIndex}}" ng-model="server.keystore.password">
+                                                            <div ng-messages="theForm['keystorePassword_' + serverIndex].$error">
+                                                                <div translate ng-message="required">extension.field-required</div>
+                                                            </div>
+                                                        </md-input-container>
+
+                                                        <md-input-container flex="50" class="md-block">
+                                                            <label translate>extension.opc-keystore-alias</label>
+                                                            <input required name="keystoreAlias_{{serverIndex}}" ng-model="server.keystore.alias">
+                                                            <div ng-messages="theForm['keystoreAlias_' + serverIndex].$error">
+                                                                <div translate ng-message="required">extension.field-required</div>
+                                                            </div>
+                                                        </md-input-container>
+                                                    </div>
+
+                                                    <md-input-container class="md-block">
+                                                        <label translate>extension.opc-keystore-key-password</label>
+                                                        <input required name="keystoreKeyPassword_{{serverIndex}}" ng-model="server.keystore.keyPassword">
+                                                        <div ng-messages="theForm['keystoreKeyPassword_' + serverIndex].$error">
+                                                            <div translate ng-message="required">extension.field-required</div>
+                                                        </div>
+                                                    </md-input-container>
+
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+
+                                        <v-accordion id="opc-mapping-accordion"
+                                                     class="vAccordion--default"
+                                                     onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                            <v-pane id="opc-mapping-pane_{{serverIndex}}">
+                                                <v-pane-header>
+                                                    {{ 'extension.mapping' | translate }}
+                                                </v-pane-header>
+                                                <v-pane-content>
+                                                    <div ng-if="server.mapping.length > 0">
+                                                        <ol class="list-group">
+                                                            <li class="list-group-item"
+                                                                ng-repeat="(mapIndex, map) in server.mapping"
+                                                            >
+                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                           class="md-icon-button"
+                                                                           ng-click="removeItem(map, server.mapping)"
+                                                                >
+                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                    <md-tooltip md-direction="top">
+                                                                        {{ 'action.remove' | translate }}
+                                                                    </md-tooltip>
+                                                                </md-button>
+
+                                                                <md-card>
+                                                                    <md-card-content>
+                                                                        <div flex layout="row">
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.opc-device-node-pattern</label>
+                                                                                <input required
+                                                                                       name="deviceNodePattern_{{serverIndex}}{{mapIndex}}"
+                                                                                       ng-model="map.deviceNodePattern"
+                                                                                >
+                                                                                <div ng-messages="theForm['deviceNodePattern_' + serverIndex + mapIndex].$error">
+                                                                                    <div translate
+                                                                                         ng-message="required"
+                                                                                    >extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+
+                                                                            <md-input-container flex="50" class="md-block">
+                                                                                <label translate>extension.opc-device-name-pattern</label>
+                                                                                <input required
+                                                                                       name="deviceNamePattern_{{serverIndex}}{{mapIndex}}"
+                                                                                       ng-model="map.deviceNamePattern"
+                                                                                >
+                                                                                <div ng-messages="theForm['deviceNamePattern_' + serverIndex + mapIndex].$error">
+                                                                                    <div translate
+                                                                                         ng-message="required"
+                                                                                    >extension.field-required</div>
+                                                                                </div>
+                                                                            </md-input-container>
+                                                                        </div>
+
+
+                                                                        <v-accordion id="opc-attributes-accordion"
+                                                                                     class="vAccordion--default"
+                                                                                     onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                                                            <v-pane id="opc-attributes-pane_{{serverIndex}}{{mapIndex}}">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.attributes' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-show="map.attributes.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item"
+                                                                                                ng-repeat="(attributeIndex, attribute) in map.attributes"
+                                                                                            >
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                                                           class="md-icon-button"
+                                                                                                           ng-click="removeItem(attribute, map.attributes)">
+                                                                                                    <ng-md-icon icon="close"
+                                                                                                                aria-label="{{ 'action.remove' | translate }}"
+                                                                                                    ></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+
+                                                                                                        <section flex
+                                                                                                                 layout="row"
+                                                                                                        >
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required
+                                                                                                                       name="opcAttributeKey_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+                                                                                                                       ng-model="attribute.key"
+                                                                                                                >
+                                                                                                                <div ng-messages="theForm['opcAttributeKey_' + serverIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40" class="md-block tb-container-for-select">
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required name="opcAttributeType_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+                                                                                                                           ng-model="attribute.type"
+                                                                                                                >
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
+                                                                                                                               ng-value="attrType"
+                                                                                                                    >
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['opcAttributeType_' + serverIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="100" class="md-block">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="opcAttributeValue_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+                                                                                                                       ng-model="attribute.value"
+                                                                                                                >
+                                                                                                                <div ng-messages="theForm['opcAttributeValue_' + serverIndex + mapIndex + attributeIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+
+                                                                                                        </section>
+
+
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addNewAttribute(map.attributes)"
+                                                                                                   aria-label="{{ 'action.add' | translate }}"
+                                                                                        >
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>extension.add-attribute</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+
+                                                                        <v-accordion id="opc-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+                                                                            <v-pane id="opc-timeseries-pane_{{serverIndex}}{{mapIndex}}">
+                                                                                <v-pane-header>
+                                                                                    {{ 'extension.timeseries' | translate }}
+                                                                                </v-pane-header>
+                                                                                <v-pane-content>
+                                                                                    <div ng-show="map.timeseries.length > 0">
+                                                                                        <ol class="list-group">
+                                                                                            <li class="list-group-item"
+                                                                                                ng-repeat="(timeseriesIndex, timeseries) in map.timeseries"
+                                                                                            >
+                                                                                                <md-button aria-label="{{ 'action.remove' | translate }}"
+                                                                                                           class="md-icon-button"
+                                                                                                           ng-click="removeItem(timeseries, map.timeseries)"
+                                                                                                >
+                                                                                                    <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+                                                                                                    <md-tooltip md-direction="top">
+                                                                                                        {{ 'action.remove' | translate }}
+                                                                                                    </md-tooltip>
+                                                                                                </md-button>
+                                                                                                <md-card>
+                                                                                                    <md-card-content>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="60" class="md-block">
+                                                                                                                <label translate>extension.key</label>
+                                                                                                                <input required
+                                                                                                                       name="opcTimeseriesKey_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
+                                                                                                                       ng-model="timeseries.key"
+                                                                                                                >
+                                                                                                                <div ng-messages="theForm['opcTimeseriesKey_' + serverIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                            <md-input-container flex="40"
+                                                                                                                                class="md-block tb-container-for-select"
+                                                                                                            >
+                                                                                                                <label translate>extension.type</label>
+                                                                                                                <md-select required
+                                                                                                                           name="opcTimeseriesType_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
+                                                                                                                           ng-model="timeseries.type"
+                                                                                                                >
+                                                                                                                    <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
+                                                                                                                               ng-value="attrType"
+                                                                                                                    >
+                                                                                                                        {{attrTypeValue | translate}}
+                                                                                                                    </md-option>
+                                                                                                                </md-select>
+                                                                                                                <div ng-messages="theForm['opcTimeseriesType_' + serverIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate
+                                                                                                                         ng-message="required"
+                                                                                                                    >extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                        <section flex layout="row">
+                                                                                                            <md-input-container flex="100" class="md-block">
+                                                                                                                <label translate>extension.value</label>
+                                                                                                                <input required name="opcTimeseriesValue_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+                                                                                                                <div ng-messages="theForm['opcTimeseriesValue_' + serverIndex + mapIndex + timeseriesIndex].$error">
+                                                                                                                    <div translate ng-message="required">extension.field-required</div>
+                                                                                                                </div>
+                                                                                                            </md-input-container>
+                                                                                                        </section>
+                                                                                                    </md-card-content>
+                                                                                                </md-card>
+                                                                                            </li>
+                                                                                        </ol>
+                                                                                    </div>
+                                                                                    <div flex layout="row" layout-align="start center">
+                                                                                        <md-button class="md-primary md-raised"
+                                                                                                   ng-click="addNewAttribute(map.timeseries)"
+                                                                                                   aria-label="{{ 'action.add' | translate }}"
+                                                                                        >
+                                                                                            <md-icon class="material-icons">add</md-icon>
+                                                                                            <span translate>extension.add-timeseries</span>
+                                                                                        </md-button>
+                                                                                    </div>
+                                                                                </v-pane-content>
+                                                                            </v-pane>
+                                                                        </v-accordion>
+
+
+                                                                    </md-card-content>
+                                                                </md-card>
+                                                            </li>
+                                                        </ol>
+                                                    </div>
+                                                    <div flex
+                                                         layout="row"
+                                                         layout-align="start center"
+                                                    >
+                                                        <md-button class="md-primary md-raised"
+                                                                   ng-click="addMap(server.mapping)"
+                                                                   aria-label="{{ 'action.add' | translate }}"
+                                                        >
+                                                            <md-icon class="material-icons">add</md-icon>
+                                                            <span translate>extension.add-map</span>
+                                                        </md-button>
+                                                    </div>
+                                                </v-pane-content>
+                                            </v-pane>
+                                        </v-accordion>
+
+                                    </md-card-content>
+                                </md-card>
+                            </li>
+                        </ol>
+
+                        <div flex
+                             layout="row"
+                             layout-align="start center"
+                        >
+                            <md-button class="md-primary md-raised"
+                                       ng-click="addServer(configuration.servers)"
+                                       aria-label="{{ 'action.add' | translate }}"
+                            >
+                                <md-icon class="material-icons">add</md-icon>
+                                <span translate>extension.opc-add-server</span>
+                            </md-button>
+                        </div>
+
+                    </div>
+                </v-pane-content>
+            </v-pane>
+        </v-accordion>
+        <!--{{config}}-->
+    </md-card-content>
+</md-card>
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js
new file mode 100644
index 0000000..24e9842
--- /dev/null
+++ b/ui/src/app/extension/extension-table.directive.js
@@ -0,0 +1,404 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'angular-material-data-table/dist/md-data-table.min.css';
+import './extension-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import extensionTableTemplate from './extension-table.tpl.html';
+import extensionDialogTemplate from './extension-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import ExtensionDialogController from './extension-dialog.controller'
+import $ from 'jquery';
+
+/*@ngInject*/
+export default function ExtensionTableDirective() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            entityId: '=',
+            entityType: '@',
+            inWidget: '@?',
+            ctx: '=?',
+            entityName: '='
+        },
+        controller: ExtensionTableController,
+        controllerAs: 'vm',
+        templateUrl: extensionTableTemplate
+    };
+}
+
+/*@ngInject*/
+function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
+
+    let vm = this;
+
+    vm.extensions = [];
+    vm.allExtensions = [];
+    vm.selectedExtensions = [];
+    vm.extensionsCount = 0;
+
+    vm.query = {
+        order: 'id',
+        limit: 5,
+        page: 1,
+        search: null
+    };
+
+    vm.enterFilterMode = enterFilterMode;
+    vm.exitFilterMode = exitFilterMode;
+    vm.onReorder = onReorder;
+    vm.onPaginate = onPaginate;
+    vm.addExtension = addExtension;
+    vm.editExtension = editExtension;
+    vm.deleteExtension = deleteExtension;
+    vm.deleteExtensions = deleteExtensions;
+    vm.reloadExtensions = reloadExtensions;
+    vm.updateExtensions = updateExtensions;
+
+    $scope.$watch("vm.entityId", function(newVal) {
+        if (newVal) {
+            if ($scope.subscriber) {
+                telemetryWebsocketService.unsubscribe($scope.subscriber);
+                $scope.subscriber = null;
+            }
+
+            vm.subscribed = false;
+            vm.syncLastTime = $translate.instant('extension.sync.not-available');
+
+            subscribeForClientAttributes();
+
+            reloadExtensions();
+        }
+    });
+
+    $scope.$on('$destroy', function() {
+        if ($scope.subscriber) {
+            telemetryWebsocketService.unsubscribe($scope.subscriber);
+            $scope.subscriber = null;
+        }
+    });
+
+    $scope.$watch("vm.query.search", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+            updateExtensions();
+        }
+    });
+
+    $scope.$watch('vm.selectedExtensions.length', function (newLength) {
+        var selectionMode = newLength ? true : false;
+        if (vm.ctx) {
+            if (selectionMode) {
+                vm.ctx.hideTitlePanel = true;
+                $scope.$emit("selectedExtensions", true);
+            } else if (vm.query.search == null) {
+                vm.ctx.hideTitlePanel = false;
+                $scope.$emit("selectedExtensions", false);
+            }
+        }
+    });
+
+    $scope.$on("showSearch", function($event, source) {
+        if(source.entityId == vm.entityId) {
+            enterFilterMode();
+            $scope.$emit("filterMode", true);
+        }
+    });
+    $scope.$on("refreshExtensions", function($event, source) {
+        if(source.entityId == vm.entityId) {
+            reloadExtensions();
+        }
+    });
+    $scope.$on("addExtension", function($event, source) {
+        if(source.entityId == vm.entityId) {
+            addExtension();
+        }
+    });
+    $scope.$on("exportExtensions", function($event, source) {
+        if(source.entityId == vm.entityId) {
+            vm.exportExtensions(source.entityName);
+        }
+    });
+    $scope.$on("importExtensions", function($event, source) {
+        if(source.entityId == vm.entityId) {
+            vm.importExtensions();
+        }
+    });
+
+    function enterFilterMode() {
+        vm.query.search = '';
+        if(vm.inWidget) {
+            vm.ctx.hideTitlePanel = true;
+        }
+    }
+
+    function exitFilterMode() {
+        vm.query.search = null;
+        updateExtensions();
+        if(vm.inWidget) {
+            vm.ctx.hideTitlePanel = false;
+            $scope.$emit("filterMode", false);
+        }
+    }
+
+    function onReorder() {
+        updateExtensions();
+    }
+
+    function onPaginate() {
+        updateExtensions();
+    }
+
+    function addExtension($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        openExtensionDialog($event);
+    }
+
+    function editExtension($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        openExtensionDialog($event, extension);
+    }
+
+    function openExtensionDialog($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var isAdd = false;
+        if(!extension) {
+            isAdd = true;
+        }
+        $mdDialog.show({
+            controller: ExtensionDialogController,
+            controllerAs: 'vm',
+            templateUrl: extensionDialogTemplate,
+            parent: angular.element($document[0].body),
+            locals: {
+                isAdd: isAdd,
+                allExtensions: vm.allExtensions,
+                entityId: vm.entityId,
+                entityType: vm.entityType,
+                extension: extension
+            },
+            bindToController: true,
+            targetEvent: $event,
+            fullscreen: true,
+            skipHide: true
+        }).then(function() {
+            reloadExtensions();
+        }, function () {
+        });
+    }
+
+    function deleteExtension($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if(extension) {
+            var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
+            var content = $translate.instant('extension.delete-extension-text');
+
+            var confirm = $mdDialog.confirm()
+                .targetEvent($event)
+                .title(title)
+                .htmlContent(content)
+                .ariaLabel(title)
+                .cancel($translate.instant('action.no'))
+                .ok($translate.instant('action.yes'));
+            $mdDialog.show(confirm).then(function() {
+                var editedExtensions = vm.allExtensions.filter(function(ext) {
+                    return ext.id !== extension.id;
+                });
+                var editedValue = angular.toJson(editedExtensions);
+                attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
+                    function success() {
+                        reloadExtensions();
+                    }
+                );
+            });
+        }
+    }
+
+    function deleteExtensions($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
+            var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
+            var content = $translate.instant('extension.delete-extensions-text');
+
+            var confirm = $mdDialog.confirm()
+                .targetEvent($event)
+                .title(title)
+                .htmlContent(content)
+                .ariaLabel(title)
+                .cancel($translate.instant('action.no'))
+                .ok($translate.instant('action.yes'));
+            $mdDialog.show(confirm).then(function () {
+                var editedExtensions = angular.copy(vm.allExtensions);
+                for (var i = 0; i < vm.selectedExtensions.length; i++) {
+                    editedExtensions = editedExtensions.filter(function (ext) {
+                        return ext.id !== vm.selectedExtensions[i].id;
+                    });
+                }
+                var editedValue = angular.toJson(editedExtensions);
+                attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
+                    function success() {
+                        reloadExtensions();
+                    }
+                );
+            });
+        }
+    }
+
+    function reloadExtensions() {
+        vm.subscribed = false;
+        vm.allExtensions.length = 0;
+        vm.extensions.length = 0;
+        vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
+        vm.extensionsPromise.then(
+            function success(data) {
+                if (data.length) {
+                    vm.allExtensions = angular.fromJson(data[0].value);
+                } else {
+                    vm.allExtensions = [];
+                }
+
+                vm.selectedExtensions = [];
+                updateExtensions();
+                vm.extensionsPromise = null;
+            },
+            function fail() {
+                vm.extensions = [];
+                vm.selectedExtensions = [];
+                updateExtensions();
+                vm.extensionsPromise = null;
+            }
+        );
+    }
+
+    function updateExtensions() {
+        vm.selectedExtensions = [];
+        var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
+        if (vm.query.search != null) {
+            result = $filter('filter')(result, function(extension) {
+                if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
+                    return true;
+                }
+                return false;
+            });
+        }
+        vm.extensionsCount = result.length;
+        var startIndex = vm.query.limit * (vm.query.page - 1);
+        vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
+
+        vm.extensionsJSON = angular.toJson(vm.extensions);
+        checkForSync();
+    }
+
+    function subscribeForClientAttributes() {
+        if (!vm.subscribed) {
+            if (vm.entityId && vm.entityType) {
+                $scope.subscriber = {
+                    subscriptionCommands: [{
+                        entityType: vm.entityType,
+                        entityId: vm.entityId,
+                        scope: 'CLIENT_SCOPE'
+                    }],
+                    type: 'attribute',
+                    onData: function (data) {
+                        if (data.data) {
+                            onSubscriptionData(data.data);
+                        }
+                        vm.subscribed = true;
+                    }
+                };
+                telemetryWebsocketService.subscribe($scope.subscriber);
+            }
+        }
+    }
+    function onSubscriptionData(data) {
+
+        if ($.isEmptyObject(data)) {
+            vm.appliedConfiguration = undefined;
+        } else {
+            if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) {
+                vm.appliedConfiguration = data.appliedConfiguration[0][1];
+            }
+        }
+
+        updateExtensions();
+        $scope.$digest();
+    }
+
+
+    function checkForSync() {
+        if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) {
+            vm.syncStatus = $translate.instant('extension.sync.sync');
+            vm.syncLastTime = formatDate();
+            $scope.isSync = true;
+        } else {
+            vm.syncStatus = $translate.instant('extension.sync.not-sync');
+
+            $scope.isSync = false;
+        }
+    }
+
+    function formatDate(date) {
+        let d;
+        if (date) {
+            d = date;
+        } else {
+            d = new Date();
+        }
+
+        d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds());
+        return d;
+
+        function addZero(num) {
+            if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) {
+                num = '0' + num;
+            }
+            return num;
+        }
+    }
+
+    vm.importExtensions = function($event) {
+        importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions});
+    };
+    vm.exportExtensions = function(widgetSourceEntityName) {
+        if(vm.inWidget) {
+            importExport.exportToPc(vm.extensionsJSON,  widgetSourceEntityName + '_configuration.json');
+        } else {
+            importExport.exportToPc(vm.extensionsJSON,  vm.entityName + '_configuration.json');
+        }
+    };
+
+    /*change function for widget implementing, like vm.exportExtensions*/
+    vm.exportExtension = function($event, extension) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        importExport.exportToPc(extension,  vm.entityName +'_'+ extension.id +'_configuration.json');
+    };
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss
new file mode 100644
index 0000000..7c0a0d8
--- /dev/null
+++ b/ui/src/app/extension/extension-table.scss
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../scss/constants';
+
+
+.extension-table {
+
+  md-input-container .md-errors-spacer {
+    min-height: 0;
+  }
+
+  /*&.tb-data-table table.md-table tbody tr td.tb-action-cell,
+  &.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell {
+    width: 114px;
+  }*/
+  .sync-widget {
+    max-height: 90px;
+    overflow: hidden;
+  }
+  .toolbar-widget {
+    min-height: 39px;
+    max-height: 39px;
+  }
+}
+
+.extension__syncStatus--black {
+  color: #000000!important;
+}
+.extension__syncStatus--green {
+  color: #228634!important;
+}
+.extension__syncStatus--red {
+  color: #862222!important;
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html
new file mode 100644
index 0000000..b86061c
--- /dev/null
+++ b/ui/src/app/extension/extension-table.tpl.html
@@ -0,0 +1,159 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+
+<md-content flex class="md-padding tb-absolute-fill tb-data-table extension-table" layout="column">
+    <div layout="column" class="md-whiteframe-z1" ng-class="{'tb-absolute-fill' : vm.inWidget}">
+        <md-toolbar ng-if="!vm.inWidget" class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
+                                                                                      && vm.query.search === null">
+            <div class="md-toolbar-tools">
+                <span translate>{{ 'extension.extensions' }}</span>
+                <span flex></span>
+
+                <md-button class="md-icon-button" ng-click="vm.importExtensions($event)">
+                    <md-icon>file_upload</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'extension.import-extensions-configuration' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.exportExtensions()">
+                    <md-icon>file_download</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'extension.export-extensions-configuration' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.addExtension($event)">
+                    <md-icon>add</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.add' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+                    <md-icon>search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-icon-button" ng-click="vm.reloadExtensions()">
+                    <md-icon>refresh</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.refresh' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
+                                                                 && vm.query.search != null" ng-class="{'toolbar-widget' : vm.inWidget}">
+            <div class="md-toolbar-tools">
+                <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+                    <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-input-container flex>
+                    <label>&nbsp;</label>
+                    <input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+                </md-input-container>
+                <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
+                    <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
+                    <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
+                        {{ 'action.close' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedExtensions.length" ng-class="{'toolbar-widget' : vm.inWidget}">
+            <div class="md-toolbar-tools">
+                <span translate
+                      translate-values="{count: vm.selectedExtensions.length}"
+                      translate-interpolation="messageformat">extension.selected-extensions</span>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.deleteExtensions($event)">
+                    <md-icon>delete</md-icon>
+                    <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
+                        {{ 'action.delete' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+
+        <div class="md-padding" flex layout="row" ng-class="{'sync-widget' : vm.inWidget}">
+            <md-input-container flex="50" class="md-block">
+                <label translate>extension.sync.status</label>
+                <input ng-model="vm.syncStatus"
+                       ng-class="{'extension__syncStatus--green':isSync, 'extension__syncStatus--red':!isSync}"
+                       disabled
+                >
+            </md-input-container>
+
+            <md-input-container flex="50" class="md-block">
+                <label translate>extension.sync.last-sync-time</label>
+                <input ng-model="vm.syncLastTime"
+                       class="extension__syncStatus--black"
+                       disabled
+                >
+            </md-input-container>
+        </div>
+
+        <md-table-container flex>
+            <table md-table md-row-select multiple="" ng-model="vm.selectedExtensions" md-progress="vm.extensionsDeferred.promise">
+                <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+                    <tr md-row>
+                        <th md-column md-order-by="id"><span translate>extension.id</span></th>
+                        <th md-column md-order-by="type"><span translate>extension.type</span></th>
+                        <th md-column><span>&nbsp</span></th>
+                    </tr>
+                </thead>
+                <tbody md-body>
+                <tr md-row md-select="extension" md-select-id="extension" md-auto-select ng-repeat="extension in vm.extensions">
+                    <td md-cell>{{ extension.id }}</td>
+                    <td md-cell>{{ extension.type }}</td>
+                    <td md-cell class="tb-action-cell">
+
+                        <!--<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.exportExtension($event, extension)">
+                            <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">file_download</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.export-extension' | translate }}
+                            </md-tooltip>
+                        </md-button>-->
+
+                        <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
+                            <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.edit' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                        <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteExtension($event, extension)">
+                            <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'extension.delete' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+            <md-divider ng-if="vm.inWidget"></md-divider>
+        </md-table-container>
+        <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
+                             md-page="vm.query.page" md-total="{{vm.extensionsCount}}"
+                             md-on-paginate="vm.onPaginate" md-page-select>
+        </md-table-pagination>
+    </div>
+
+</md-content>
\ No newline at end of file
diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js
new file mode 100644
index 0000000..acb97be
--- /dev/null
+++ b/ui/src/app/extension/index.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ExtensionTableDirective from './extension-table.directive';
+import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
+import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
+import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
+import {ParseToNull} from './extension-dialog.controller';
+
+export default angular.module('thingsboard.extension', [])
+    .directive('tbExtensionTable', ExtensionTableDirective)
+    .directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
+    .directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
+    .directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
+    .directive('parseToNull', ParseToNull)
+    .name;
\ No newline at end of file
diff --git a/ui/src/app/home/home-links.controller.js b/ui/src/app/home/home-links.controller.js
index 273433b..b2b9366 100644
--- a/ui/src/app/home/home-links.controller.js
+++ b/ui/src/app/home/home-links.controller.js
@@ -13,8 +13,44 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+import './home-links.scss';
+
 /*@ngInject*/
-export default function HomeLinksController($scope, menu) {
+export default function HomeLinksController($scope, $mdMedia, menu) {
+
     var vm = this;
+
+    vm.sectionColspan = sectionColspan;
+
+    $scope.$watch(function() { return $mdMedia('lg'); }, function() {
+        updateColumnCount();
+    });
+
+    $scope.$watch(function() { return $mdMedia('gt-lg'); }, function() {
+        updateColumnCount();
+    });
+
+    updateColumnCount();
+
     vm.model = menu.getHomeSections();
+
+    function updateColumnCount() {
+        vm.cols = 2;
+        if ($mdMedia('lg')) {
+            vm.cols = 3;
+        }
+        if ($mdMedia('gt-lg')) {
+            vm.cols = 4;
+        }
+    }
+
+    function sectionColspan(section) {
+        var colspan = vm.cols;
+        if (section && section.places && section.places.length <= colspan) {
+            colspan = section.places.length;
+        }
+        return colspan;
+    }
+
 }
diff --git a/ui/src/app/home/home-links.scss b/ui/src/app/home/home-links.scss
new file mode 100644
index 0000000..e210334
--- /dev/null
+++ b/ui/src/app/home/home-links.scss
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "../../scss/constants";
+
+.tb-home-links {
+    .md-headline {
+      font-size: 20px;
+      @media (min-width: $layout-breakpoint-xmd) {
+        font-size: 24px;
+      }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/app/home/home-links.tpl.html b/ui/src/app/home/home-links.tpl.html
index 5eac69f..f0e5582 100644
--- a/ui/src/app/home/home-links.tpl.html
+++ b/ui/src/app/home/home-links.tpl.html
@@ -15,8 +15,8 @@
     limitations under the License.
 
 -->
-<md-grid-list md-cols="2" md-cols-gt-sm="4" md-row-height="280px">
-	<md-grid-tile md-colspan="2" md-colspan-gt-sm="{{section.places.length}}" ng-repeat="section in vm.model">
+<md-grid-list class="tb-home-links" md-cols="{{vm.cols}}" md-row-height="280px">
+	<md-grid-tile md-colspan="2" md-colspan-gt-sm="{{vm.sectionColspan(section)}}" ng-repeat="section in vm.model">
 		<md-card style='width: 100%;'>
 			<md-card-title>
 				<md-card-title-text>
@@ -25,12 +25,12 @@
 			</md-card-title>
 			<md-card-content>
 				<md-grid-list md-row-height="170px" md-cols="{{section.places.length}}" md-cols-gt-md="{{section.places.length}}">
-					 <md-grid-tile class="card-tile" ng-repeat="place in section.places">
-			        	<md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
-			        		<md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
-			   				<span translate>{{place.name}}</span>
-			        	</md-button>
-					 </md-grid-tile>
+					<md-grid-tile class="card-tile" ng-repeat="place in section.places">
+						<md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
+							<md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
+							<span translate>{{place.name}}</span>
+						</md-button>
+					</md-grid-tile>
 				</md-grid-list>
 			</md-card-content>
 		</md-card>
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 86d5240..62e1bb3 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -24,8 +24,9 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
 /* eslint-disable no-undef, angular/window-service, angular/document-service */
 
 /*@ngInject*/
-export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, utils, types, dashboardUtils,
-                                     entityService, dashboardService, pluginService, ruleService, widgetService, toast) {
+export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
+                                     dashboardUtils, entityService, dashboardService, pluginService, ruleService,
+                                     widgetService, toast, attributeService) {
 
 
     var service = {
@@ -40,8 +41,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         exportWidgetType: exportWidgetType,
         importWidgetType: importWidgetType,
         exportWidgetsBundle: exportWidgetsBundle,
-        importWidgetsBundle: importWidgetsBundle
-    }
+        importWidgetsBundle: importWidgetsBundle,
+        exportExtension: exportExtension,
+        importExtension: importExtension,
+        exportToPc: exportToPc
+    };
 
     return service;
 
@@ -614,6 +618,84 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
+
+
+    function exportExtension(extensionId) {
+
+        getExtension(extensionId)
+            .then(
+                function success(extension) {
+                    var name = extension.title;
+                    name = name.toLowerCase().replace(/\W/g,"_");
+                    exportToPc(prepareExport(extension), name + '.json');
+                },
+                function fail(rejection) {
+                    var message = rejection;
+                    if (!message) {
+                        message = $translate.instant('error.unknown-error');
+                    }
+                    toast.showError($translate.instant('extension.export-failed-error', {error: message}));
+                }
+            );
+
+        function getExtension(extensionId) {
+            var deferred = $q.defer();
+            var url = '/api/plugins/telemetry/DEVICE/' + extensionId;
+            $http.get(url, null)
+                .then(function success(response) {
+                    deferred.resolve(response.data);
+                }, function fail() {
+                    deferred.reject();
+                });
+            return deferred.promise;
+        }
+
+    }
+
+    function importExtension($event, options) {
+        var deferred = $q.defer();
+        openImportDialog($event, 'extension.import-extensions', 'extension.file')
+            .then(
+                function success(extension) {
+                    if (!validateImportedExtension(extension)) {
+                        toast.showError($translate.instant('extension.invalid-file-error'));
+                        deferred.reject();
+                    } else {
+                        attributeService
+                            .saveEntityAttributes(
+                                options.entityType,
+                                options.entityId,
+                                types.attributesScope.shared.value,
+                                [{
+                                    key: "configuration",
+                                    value: angular.toJson(extension)
+                                }]
+                            )
+                            .then(function success() {
+                                options.successFunc();
+                            });
+                    }
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        return deferred.promise;
+    }
+
+    function validateImportedExtension(configuration) {
+        if (configuration.length) {
+            for (let i = 0; i < configuration.length; i++) {
+                if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) {
+                    return false;
+                }
+            }
+        } else {
+            return false;
+        }
+        return true;
+    }
+
     function processEntityAliases(entityAliases, aliasIds) {
         var deferred = $q.defer();
         var missingEntityAliases = {};
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index 9d3e1d1..c23b008 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
 import thingsboardEntity from '../entity';
 import thingsboardEvent from '../event';
 import thingsboardAlarm from '../alarm';
+import thingsboardExtension from '../extension';
 import thingsboardTenant from '../tenant';
 import thingsboardCustomer from '../customer';
 import thingsboardUser from '../user';
@@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [
     thingsboardEntity,
     thingsboardEvent,
     thingsboardAlarm,
+    thingsboardExtension,
     thingsboardTenant,
     thingsboardCustomer,
     thingsboardUser,
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index bd1b9e9..ab672de 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -729,6 +729,133 @@ export default angular.module('thingsboard.locale', [])
                     "messages-processed": "Messages processed",
                     "errors-occurred": "Errors occurred"
                 },
+                "extension": {
+                    "extensions": "Extensions",
+                    "selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
+                    "type": "Type",
+                    "key": "Key",
+                    "value": "Value",
+                    "id": "Id",
+                    "extension-id": "Extension id",
+                    "extension-type": "Extension type",
+                    "transformer-json": "JSON *",
+                    "unique-id-required": "Current extension id already exists.",
+                    "delete": "Delete extension",
+                    "add": "Add extension",
+                    "edit": "Edit extension",
+                    "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
+                    "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
+                    "delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
+                    "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
+                    "converters": "Converters",
+                    "converter-id": "Converter id",
+                    "configuration": "Configuration",
+                    "converter-configurations": "Converter configurations",
+                    "token": "Security token",
+                    "add-converter": "Add converter",
+                    "add-config": "Add converter configuration",
+                    "device-name-expression": "Device name expression",
+                    "device-type-expression": "Device type expression",
+                    "custom": "Custom",
+                    "to-double": "To Double",
+                    "transformer": "Transformer",
+                    "json-required": "Transformer json is required.",
+                    "json-parse": "Unable to parse transformer json.",
+                    "attributes": "Attributes",
+                    "add-attribute": "Add attribute",
+                    "add-map": "Add mapping element",
+                    "timeseries": "Timeseries",
+                    "add-timeseries": "Add timeseries",
+                    "field-required": "Field is required",
+                    "brokers": "Brokers",
+                    "add-broker": "Add broker",
+                    "host": "Host",
+                    "port": "Port",
+                    "port-range": "Port should be in a range from 1 to 65535.",
+                    "ssl": "Ssl",
+                    "credentials": "Credentials",
+                    "username": "Username",
+                    "password": "Password",
+                    "retry-interval": "Retry interval in milliseconds",
+                    "anonymous": "Anonymous",
+                    "basic": "Basic",
+                    "pem": "PEM",
+                    "ca-cert": "CA certificate file *",
+                    "private-key": "Private key file *",
+                    "cert": "Certificate file *",
+                    "no-file": "No file selected.",
+                    "drop-file": "Drop a file or click to select a file to upload.",
+                    "mapping": "Mapping",
+                    "topic-filter": "Topic filter",
+                    "converter-type": "Converter type",
+                    "converter-json": "Json",
+                    "json-name-expression": "Device name json expression",
+                    "topic-name-expression": "Device name topic expression",
+                    "json-type-expression": "Device type json expression",
+                    "topic-type-expression": "Device type topic expression",
+                    "attribute-key-expression": "Attribute key expression",
+                    "attr-json-key-expression": "Attribute key json expression",
+                    "attr-topic-key-expression": "Attribute key topic expression",
+                    "request-id-expression": "Request id expression",
+                    "request-id-json-expression": "Request id json expression",
+                    "request-id-topic-expression": "Request id topic expression",
+                    "response-topic-expression": "Response topic expression",
+                    "value-expression": "Value expression",
+                    "topic": "Topic",
+                    "timeout": "Timeout in milliseconds",
+                    "converter-json-required": "Converter json is required.",
+                    "converter-json-parse": "Unable to parse converter json.",
+                    "filter-expression": "Filter expression",
+                    "connect-requests": "Connect requests",
+                    "add-connect-request": "Add connect request",
+                    "disconnect-requests": "Disconnect requests",
+                    "add-disconnect-request": "Add disconnect request",
+                    "attribute-requests": "Attribute requests",
+                    "add-attribute-request": "Add attribute request",
+                    "attribute-updates": "Attribute updates",
+                    "add-attribute-update": "Add attribute update",
+                    "server-side-rpc": "Server side RPC",
+                    "add-server-side-rpc-request": "Add server-side RPC request",
+                    "device-name-filter": "Device name filter",
+                    "attribute-filter": "Attribute filter",
+                    "method-filter": "Method filter",
+                    "request-topic-expression": "Request topic expression",
+                    "response-timeout": "Response timeout in milliseconds",
+                    "topic-expression": "Topic expression",
+                    "client-scope": "Client scope",
+                    "opc-server": "Servers",
+                    "opc-add-server": "Add server",
+                    "opc-application-name": "Application name",
+                    "opc-application-uri": "Application uri",
+                    "opc-scan-period-in-seconds": "Scan period in seconds",
+                    "opc-security": "Security",
+                    "opc-identity": "Identity",
+                    "opc-keystore": "Keystore",
+                    "opc-type": "Type",
+                    "opc-keystore-type":"Type",
+                    "opc-keystore-location":"Location *",
+                    "opc-keystore-password":"Password",
+                    "opc-keystore-alias":"Alias",
+                    "opc-keystore-key-password":"Key password",
+                    "opc-device-node-pattern":"Device node pattern",
+                    "opc-device-name-pattern":"Device name pattern",
+
+                    "sync": {
+                        "status": "Status",
+                        "sync": "Sync",
+                        "not-sync": "Not sync",
+                        "last-sync-time": "Last sync time",
+                        "not-available": "Not available"
+                    },
+
+                    "export-extensions-configuration":"Export extensions configuration",
+                    "import-extensions-configuration":"Import extensions configuration",
+                    "import-extensions": "Import extensions",
+                    "import-extension": "Import extension",
+                    "export-extension": "Export extension",
+                    "file": "Extensions file",
+                    "invalid-file-error": "Invalid extension file"
+                },
                 "fullscreen": {
                     "expand": "Expand to fullscreen",
                     "exit": "Exit fullscreen",
@@ -851,7 +978,6 @@ export default angular.module('thingsboard.locale', [])
                     "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
                     "copyId": "Copy plugin Id",
                     "idCopiedMessage": "Plugin Id has been copied to clipboard"
-
                 },
                 "position": {
                     "top": "Top",
@@ -1071,7 +1197,8 @@ export default angular.module('thingsboard.locale', [])
                     "boolean": "Boolean",
                     "boolean-value": "Boolean value",
                     "false": "False",
-                    "true": "True"
+                    "true": "True",
+                    "long": "Long"
                 },
                 "widget": {
                     "widget-library": "Widgets Library",
diff --git a/ui/src/app/widget/lib/analogue-compass.js b/ui/src/app/widget/lib/analogue-compass.js
index 46059c7..85ec0a6 100644
--- a/ui/src/app/widget/lib/analogue-compass.js
+++ b/ui/src/app/widget/lib/analogue-compass.js
@@ -104,7 +104,9 @@ export default class TbAnalogueCompass {
                 var tvPair = cellData.data[cellData.data.length -
                 1];
                 var value = tvPair[1];
-                this.gauge.value = value;
+                if(value !== this.gauge.value) {
+                    this.gauge.value = value;
+                }
             }
         }
     }
diff --git a/ui/src/app/widget/lib/analogue-linear-gauge.js b/ui/src/app/widget/lib/analogue-linear-gauge.js
index a045f94..da4e386 100644
--- a/ui/src/app/widget/lib/analogue-linear-gauge.js
+++ b/ui/src/app/widget/lib/analogue-linear-gauge.js
@@ -212,7 +212,9 @@ export default class TbAnalogueLinearGauge {
                 var tvPair = cellData.data[cellData.data.length -
                 1];
                 var value = tvPair[1];
-                this.gauge.value = value;
+                if(value !== this.gauge.value) {
+                    this.gauge.value = value;
+                }
             }
         }
     }
diff --git a/ui/src/app/widget/lib/analogue-radial-gauge.js b/ui/src/app/widget/lib/analogue-radial-gauge.js
index 76a2c38..7a5c0c7 100644
--- a/ui/src/app/widget/lib/analogue-radial-gauge.js
+++ b/ui/src/app/widget/lib/analogue-radial-gauge.js
@@ -221,7 +221,9 @@ export default class TbAnalogueRadialGauge {
                 var tvPair = cellData.data[cellData.data.length -
                 1];
                 var value = tvPair[1];
-                this.gauge.value = value;
+                if(value !== this.gauge.value) {
+                    this.gauge.value = value;
+                }
             }
         }
 
diff --git a/ui/src/app/widget/lib/canvas-digital-gauge.js b/ui/src/app/widget/lib/canvas-digital-gauge.js
index 74ab7ac..231fb6f 100644
--- a/ui/src/app/widget/lib/canvas-digital-gauge.js
+++ b/ui/src/app/widget/lib/canvas-digital-gauge.js
@@ -197,14 +197,19 @@ export default class TbCanvasDigitalGauge {
             if (cellData.data.length > 0) {
                 var tvPair = cellData.data[cellData.data.length -
                 1];
+                var timestamp;
                 if (this.localSettings.showTimestamp) {
-                    var timestamp = tvPair[0];
+                    timestamp = tvPair[0];
                     var filter= this.ctx.$scope.$injector.get('$filter');
                     var timestampDisplayValue = filter('date')(timestamp, this.localSettings.timestampFormat);
                     this.gauge.options.label = timestampDisplayValue;
                 }
                 var value = tvPair[1];
-                this.gauge.value = value;
+                if(value !== this.gauge.value) {
+                    this.gauge.value = value;
+                } else if (this.localSettings.showTimestamp && this.gauge.timestamp != timestamp) {
+                    this.gauge.timestamp = timestamp;
+                }
             }
         }
     }
diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js
index a1cfca2..bd3ff98 100644
--- a/ui/src/app/widget/lib/CanvasDigitalGauge.js
+++ b/ui/src/app/widget/lib/CanvasDigitalGauge.js
@@ -155,6 +155,15 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
         return result;
     }
 
+    set timestamp(timestamp) {
+        this.options.timestamp = timestamp;
+        this.draw();
+    }
+
+    get timestamp() {
+        return this.options.timestamp;
+    }
+
     draw() {
         try {
 
@@ -195,7 +204,9 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
                 canvas.elementClone.initialized = true;
             }
 
-            if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value) {
+            var valueChanged = false;
+
+            if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) {
                 let context = this.contextValueClone;
                 // clear the cache
                 context.clearRect(x, y, w, h);
@@ -208,10 +219,13 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
 
                 if (options.showTimestamp) {
                     drawDigitalLabel(context, options);
+                    this.elementValueClone.renderedTimestamp = this.timestamp;
                 }
 
                 this.elementValueClone.initialized = true;
                 this.elementValueClone.renderedValue = this.value;
+
+                valueChanged = true;
             }
 
             var progress = (canvasGauges.drawings.normalizedValue(options).normal - options.minValue) /
@@ -219,7 +233,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
 
             var fixedProgress = progress.toFixed(3);
 
-            if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress) {
+            if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress || valueChanged) {
                 let context = this.contextProgressClone;
                 // clear the cache
                 context.clearRect(x, y, w, h);
diff --git a/ui/src/app/widget/lib/extensions-table-widget.js b/ui/src/app/widget/lib/extensions-table-widget.js
new file mode 100644
index 0000000..3e5ac61
--- /dev/null
+++ b/ui/src/app/widget/lib/extensions-table-widget.js
@@ -0,0 +1,135 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './extensions-table-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import extensionsTableWidgetTemplate from './extensions-table-widget.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.widgets.extensionsTableWidget', [])
+    .directive('tbExtensionsTableWidget', ExtensionsTableWidget)
+    .name;
+
+/*@ngInject*/
+function ExtensionsTableWidget() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            ctx: '='
+        },
+        controller: ExtensionsTableWidgetController,
+        controllerAs: 'vm',
+        templateUrl: extensionsTableWidgetTemplate
+    };
+}
+
+/*@ngInject*/
+function ExtensionsTableWidgetController($scope, $translate, utils) {
+    var vm = this;
+
+    vm.datasources = null;
+    vm.tabsHidden = false;
+
+    $scope.$watch('vm.ctx', function() {
+        if (vm.ctx && vm.ctx.defaultSubscription) {
+            vm.settings = vm.ctx.settings;
+            vm.subscription = vm.ctx.defaultSubscription;
+            vm.datasources = vm.subscription.datasources;
+            initializeConfig();
+            updateDatasources();
+        }
+    });
+
+    function initializeConfig() {
+
+        if (vm.settings.extensionsTitle && vm.settings.extensionsTitle.length) {
+            vm.extensionsTitle = utils.customTranslation(vm.settings.extensionsTitle, vm.settings.extensionsTitle);
+        } else {
+            vm.extensionsTitle = $translate.instant('extension.extensions');
+        }
+        vm.ctx.widgetTitle = vm.extensionsTitle;
+
+        vm.ctx.widgetActions = [vm.importExtensionsAction, vm.exportExtensionsAction, vm.addAction, vm.searchAction, vm.refreshAction];
+    }
+
+    function updateDatasources() {
+
+        var datasource = vm.datasources[0];
+        vm.selectedSource = vm.datasources[0];
+        vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.extensionsTitle);
+    }
+
+    vm.changeSelectedSource = function(source) {
+        vm.selectedSource = source;
+    };
+
+    vm.searchAction = {
+        name: "action.search",
+        show: true,
+        onAction: function() {
+            $scope.$broadcast("showSearch", vm.selectedSource);
+        },
+        icon: "search"
+    };
+
+    vm.refreshAction = {
+        name: "action.refresh",
+        show: true,
+        onAction: function() {
+            $scope.$broadcast("refreshExtensions", vm.selectedSource);
+        },
+        icon: "refresh"
+    };
+
+    vm.addAction = {
+        name: "action.add",
+        show: true,
+        onAction: function() {
+            $scope.$broadcast("addExtension", vm.selectedSource);
+        },
+        icon: "add"
+    };
+
+    vm.exportExtensionsAction = {
+        name: "extension.export-extensions-configuration",
+        show: true,
+        onAction: function() {
+            $scope.$broadcast("exportExtensions", vm.selectedSource);
+        },
+        icon: "file_download"
+    };
+
+    vm.importExtensionsAction = {
+        name: "extension.import-extensions-configuration",
+        show: true,
+        onAction: function() {
+            $scope.$broadcast("importExtensions", vm.selectedSource);
+        },
+        icon: "file_upload"
+    };
+
+    $scope.$on("filterMode", function($event, mode) {
+        vm.tabsHidden = mode;
+    });
+
+    $scope.$on("selectedExtensions", function($event, mode) {
+        vm.tabsHidden = mode;
+    });
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/extensions-table-widget.scss b/ui/src/app/widget/lib/extensions-table-widget.scss
new file mode 100644
index 0000000..26ab5d9
--- /dev/null
+++ b/ui/src/app/widget/lib/extensions-table-widget.scss
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+tb-extension-table {
+  md-content {
+    background-color: #fff;
+  }
+}
+md-tabs.hide-tabs-menu {
+  md-tabs-wrapper {
+    display: none;
+  }
+  md-tabs-content-wrapper {
+    top: 0 !important;
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/extensions-table-widget.tpl.html b/ui/src/app/widget/lib/extensions-table-widget.tpl.html
new file mode 100644
index 0000000..0671938
--- /dev/null
+++ b/ui/src/app/widget/lib/extensions-table-widget.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-tabs id="tabs" md-border-bottom flex class="tb-absolute-fill" ng-class="{'hide-tabs-menu': vm.datasources.length == 1 || vm.tabsHidden}">
+    <md-tab ng-repeat="source in vm.datasources" label="{{ source.name }}" md-on-select="vm.changeSelectedSource(source)">
+        <tb-extension-table flex
+                            entity-id="source.entityId"
+                            entity-type="{{source.entityType}}"
+                            in-widget="true"
+                            ctx="vm.ctx">
+        </tb-extension-table>
+    </md-tab>
+</md-tabs>
\ No newline at end of file
diff --git a/ui/src/index.html b/ui/src/index.html
index 2f4045a..33702cc 100644
--- a/ui/src/index.html
+++ b/ui/src/index.html
@@ -16,7 +16,7 @@
 
 -->
 <!DOCTYPE html>
-<html ng-app="thingsboard" ng-strict-di>
+<html ng-app="thingsboard" ng-strict-di style="width: 100%;">
   <head>
     <title ng-bind="pageTitle"></title>
     <base href="/" />
diff --git a/ui/src/scss/animations.scss b/ui/src/scss/animations.scss
index e7d9820..ab27252 100644
--- a/ui/src/scss/animations.scss
+++ b/ui/src/scss/animations.scss
@@ -39,6 +39,6 @@
 @include keyframes(tbMoveToBottomFade) {
   to {
     opacity: 0;
-    @include transform(translate(0, 100%));
+    @include transform(translate(0, 150%));
   }
 }
\ No newline at end of file
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 076dbef..93418b2 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -494,11 +494,15 @@ md-tabs.tb-headless {
   height: 100%;
   max-width: 240px;
   span {
-    padding: 10px 10px 20px 10px;
+    padding: 0 0 20px 0;
     font-size: 18px;
     font-weight: 400;
     white-space: normal;
     line-height: 18px;
+    max-height: 18px;
+    min-height: 18px;
+    height: 18px;
+    margin: auto;
   }
 }