thingsboard-memoizeit

Changes

docker/.env 6(+6 -0)

docker/deploy_cassandra_zookeeper.sh 31(+0 -31)

docker/tb.env 3(+2 -1)

docker/tb/Makefile 12(+12 -0)

docker/tb/tb.yaml 121(+121 -0)

docker/thingsboard-db-schema.env 5(+0 -5)

pom.xml 2(+1 -1)

ui/src/app/components/datasource-device.directive.js 248(+0 -248)

ui/src/app/components/datasource-device.tpl.html 137(+0 -137)

ui/src/app/components/device-alias-select.directive.js 144(+0 -144)

ui/src/app/components/device-alias-select.tpl.html 52(+0 -52)

ui/src/app/components/device-filter.directive.js 217(+0 -217)

ui/src/app/components/device-filter.tpl.html 67(+0 -67)

ui/src/app/dashboard/aliases-device-select.directive.js 153(+0 -153)

ui/src/app/dashboard/aliases-device-select-button.tpl.html 32(+0 -32)

ui/src/app/dashboard/device-aliases.controller.js 227(+0 -227)

ui/src/app/dashboard/device-aliases.tpl.html 94(+0 -94)

ui/src/app/device/attribute/add-attribute-dialog.tpl.html 95(+0 -95)

ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js 73(+0 -73)

ui/src/app/device/attribute/attribute-table.directive.js 421(+0 -421)

ui/src/app/device/attribute/attribute-table.tpl.html 210(+0 -210)

ui/src/app/device/attribute/edit-attribute-value.controller.js 71(+0 -71)

ui/src/app/device/attribute/edit-attribute-value.tpl.html 72(+0 -72)

Details

diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
new file mode 100644
index 0000000..7a2c273
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -0,0 +1,129 @@
+/**
+ * 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.controller;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.dao.asset.AssetSearchQuery;
+import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/api")
+public class AlarmController extends BaseController {
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET)
+    @ResponseBody
+    public Alarm getAlarmById(@PathVariable("alarmId") String strAlarmId) throws ThingsboardException {
+        checkParameter("alarmId", strAlarmId);
+        try {
+            AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
+            return checkAlarmId(alarmId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm", method = RequestMethod.POST)
+    @ResponseBody
+    public Alarm saveAlarm(@RequestBody Alarm alarm) throws ThingsboardException {
+        try {
+            alarm.setTenantId(getCurrentUser().getTenantId());
+            return checkNotNull(alarmService.createOrUpdateAlarm(alarm));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST)
+    @ResponseStatus(value = HttpStatus.OK)
+    public void ackAlarm(@PathVariable("alarmId") String strAlarmId) throws ThingsboardException {
+        checkParameter("alarmId", strAlarmId);
+        try {
+            AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
+            checkAlarmId(alarmId);
+            alarmService.ackAlarm(alarmId, System.currentTimeMillis()).get();
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST)
+    @ResponseStatus(value = HttpStatus.OK)
+    public void clearAlarm(@PathVariable("alarmId") String strAlarmId) throws ThingsboardException {
+        checkParameter("alarmId", strAlarmId);
+        try {
+            AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
+            checkAlarmId(alarmId);
+            alarmService.clearAlarm(alarmId, System.currentTimeMillis()).get();
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
+    @ResponseBody
+    public TimePageData<Alarm> getAlarms(
+            @PathVariable("entityType") String strEntityType,
+            @PathVariable("entityId") String strEntityId,
+            @RequestParam(required = false) String status,
+            @RequestParam int limit,
+            @RequestParam(required = false) Long startTime,
+            @RequestParam(required = false) Long endTime,
+            @RequestParam(required = false, defaultValue = "false") boolean ascOrder,
+            @RequestParam(required = false) String offset
+    ) throws ThingsboardException {
+        checkParameter("EntityId", strEntityId);
+        checkParameter("EntityType", strEntityType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
+        AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
+        checkEntityId(entityId);
+        try {
+            TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
+            return checkNotNull(alarmService.findAlarms(new AlarmQuery(entityId, pageLink, alarmStatus)).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 24497d4..dd43e1c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -21,6 +21,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -136,13 +137,18 @@ public class AssetController extends BaseController {
     @ResponseBody
     public TextPageData<Asset> getTenantAssets(
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
         try {
             TenantId tenantId = getCurrentUser().getTenantId();
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(assetService.findAssetsByTenantIdAndType(tenantId, type, pageLink));
+            } else {
+                return checkNotNull(assetService.findAssetsByTenantId(tenantId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -167,6 +173,7 @@ public class AssetController extends BaseController {
     public TextPageData<Asset> getCustomerAssets(
             @PathVariable("customerId") String strCustomerId,
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
@@ -176,7 +183,11 @@ public class AssetController extends BaseController {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
             checkCustomerId(customerId);
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
+            } else {
+                return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -231,4 +242,18 @@ public class AssetController extends BaseController {
             throw handleException(e);
         }
     }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/asset/types", method = RequestMethod.GET)
+    @ResponseBody
+    public List<TenantAssetType> getAssetTypes() throws ThingsboardException {
+        try {
+            SecurityUser user = getCurrentUser();
+            TenantId tenantId = user.getTenantId();
+            ListenableFuture<List<TenantAssetType>> assetTypes = assetService.findAssetTypesByTenantId(tenantId);
+            return checkNotNull(assetTypes.get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index d4adebe..aba4b6e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -25,6 +25,8 @@ import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.thingsboard.server.actors.service.ActorService;
 import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -36,6 +38,7 @@ import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
+import org.thingsboard.server.dao.alarm.AlarmService;
 import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -84,6 +87,9 @@ public abstract class BaseController {
     protected AssetService assetService;
 
     @Autowired
+    protected AlarmService alarmService;
+
+    @Autowired
     protected DeviceCredentialsService deviceCredentialsService;
 
     @Autowired
@@ -334,6 +340,22 @@ public abstract class BaseController {
         }
     }
 
+    Alarm checkAlarmId(AlarmId alarmId) throws ThingsboardException {
+        try {
+            validateId(alarmId, "Incorrect alarmId " + alarmId);
+            Alarm alarm = alarmService.findAlarmByIdAsync(alarmId).get();
+            checkAlarm(alarm);
+            return alarm;
+        } catch (Exception e) {
+            throw handleException(e, false);
+        }
+    }
+
+    protected void checkAlarm(Alarm alarm) throws ThingsboardException {
+        checkNotNull(alarm);
+        checkTenantId(alarm.getTenantId());
+    }
+
     WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, boolean modify) throws ThingsboardException {
         try {
             validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index 7cd381c..8257767 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -21,6 +21,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -166,13 +167,18 @@ public class DeviceController extends BaseController {
     @ResponseBody
     public TextPageData<Device> getTenantDevices(
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
         try {
             TenantId tenantId = getCurrentUser().getTenantId();
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(deviceService.findDevicesByTenantId(tenantId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(deviceService.findDevicesByTenantIdAndType(tenantId, type, pageLink));
+            } else {
+                return checkNotNull(deviceService.findDevicesByTenantId(tenantId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -197,6 +203,7 @@ public class DeviceController extends BaseController {
     public TextPageData<Device> getCustomerDevices(
             @PathVariable("customerId") String strCustomerId,
             @RequestParam int limit,
+            @RequestParam(required = false) String type,
             @RequestParam(required = false) String textSearch,
             @RequestParam(required = false) String idOffset,
             @RequestParam(required = false) String textOffset) throws ThingsboardException {
@@ -206,7 +213,11 @@ public class DeviceController extends BaseController {
             CustomerId customerId = new CustomerId(toUUID(strCustomerId));
             checkCustomerId(customerId);
             TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
-            return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            if (type != null && type.trim().length()>0) {
+                return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
+            } else {
+                return checkNotNull(deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+            }
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -261,4 +272,19 @@ public class DeviceController extends BaseController {
             throw handleException(e);
         }
     }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/device/types", method = RequestMethod.GET)
+    @ResponseBody
+    public List<TenantDeviceType> getDeviceTypes() throws ThingsboardException {
+        try {
+            SecurityUser user = getCurrentUser();
+            TenantId tenantId = user.getTenantId();
+            ListenableFuture<List<TenantDeviceType>> deviceTypes = deviceService.findDeviceTypesByTenantId(tenantId);
+            return checkNotNull(deviceTypes.get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 0c1fd8b..3ddc597 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -21,6 +21,8 @@ import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationInfo;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.relation.EntityRelationsQuery;
 import org.thingsboard.server.exception.ThingsboardErrorCode;
 import org.thingsboard.server.exception.ThingsboardException;
@@ -32,7 +34,7 @@ import java.util.List;
 @RequestMapping("/api")
 public class EntityRelationController extends BaseController {
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relation", method = RequestMethod.POST)
     @ResponseStatus(value = HttpStatus.OK)
     public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException {
@@ -40,17 +42,22 @@ public class EntityRelationController extends BaseController {
             checkNotNull(relation);
             checkEntityId(relation.getFrom());
             checkEntityId(relation.getTo());
+            if (relation.getTypeGroup() == null) {
+                relation.setTypeGroup(RelationTypeGroup.COMMON);
+            }
             relationService.saveRelation(relation).get();
         } catch (Exception e) {
             throw handleException(e);
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {"fromId", "fromType", "relationType", "toId", "toType"})
     @ResponseStatus(value = HttpStatus.OK)
     public void deleteRelation(@RequestParam("fromId") String strFromId,
-                               @RequestParam("fromType") String strFromType, @RequestParam("relationType") String strRelationType,
+                               @RequestParam("fromType") String strFromType,
+                               @RequestParam("relationType") String strRelationType,
+                               @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
                                @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
         checkParameter("fromId", strFromId);
         checkParameter("fromType", strFromType);
@@ -61,8 +68,9 @@ public class EntityRelationController extends BaseController {
         EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
         checkEntityId(fromId);
         checkEntityId(toId);
+        RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            Boolean found = relationService.deleteRelation(fromId, toId, strRelationType).get();
+            Boolean found = relationService.deleteRelation(fromId, toId, strRelationType, relationTypeGroup).get();
             if (!found) {
                 throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
             }
@@ -71,7 +79,7 @@ public class EntityRelationController extends BaseController {
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"})
     @ResponseStatus(value = HttpStatus.OK)
     public void deleteRelations(@RequestParam("entityId") String strId,
@@ -87,11 +95,13 @@ public class EntityRelationController extends BaseController {
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType", "toId", "toType"})
     @ResponseStatus(value = HttpStatus.OK)
     public void checkRelation(@RequestParam("fromId") String strFromId,
-                              @RequestParam("fromType") String strFromType, @RequestParam("relationType") String strRelationType,
+                              @RequestParam("fromType") String strFromType,
+                              @RequestParam("relationType") String strRelationType,
+                              @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup,
                               @RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
         try {
             checkParameter("fromId", strFromId);
@@ -103,7 +113,8 @@ public class EntityRelationController extends BaseController {
             EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
             checkEntityId(fromId);
             checkEntityId(toId);
-            Boolean found = relationService.checkRelation(fromId, toId, strRelationType).get();
+            RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
+            Boolean found = relationService.checkRelation(fromId, toId, strRelationType, typeGroup).get();
             if (!found) {
                 throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
             }
@@ -112,71 +123,119 @@ public class EntityRelationController extends BaseController {
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType"})
     @ResponseBody
-    public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType) throws ThingsboardException {
+    public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
+                                           @RequestParam("fromType") String strFromType,
+                                           @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
+        checkParameter("fromId", strFromId);
+        checkParameter("fromType", strFromType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
+        checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
+        try {
+            return checkNotNull(relationService.findByFrom(entityId, typeGroup).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"fromId", "fromType"})
+    @ResponseBody
+    public List<EntityRelationInfo> findInfoByFrom(@RequestParam("fromId") String strFromId,
+                                                   @RequestParam("fromType") String strFromType,
+                                                   @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("fromId", strFromId);
         checkParameter("fromType", strFromType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByFrom(entityId).get());
+            return checkNotNull(relationService.findInfoByFrom(entityId, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"fromId", "fromType", "relationType"})
     @ResponseBody
-    public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId, @RequestParam("fromType") String strFromType
-            , @RequestParam("relationType") String strRelationType) throws ThingsboardException {
+    public List<EntityRelation> findByFrom(@RequestParam("fromId") String strFromId,
+                                           @RequestParam("fromType") String strFromType,
+                                           @RequestParam("relationType") String strRelationType,
+                                           @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("fromId", strFromId);
         checkParameter("fromType", strFromType);
         checkParameter("relationType", strRelationType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByFromAndType(entityId, strRelationType).get());
+            return checkNotNull(relationService.findByFromAndType(entityId, strRelationType, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType"})
     @ResponseBody
-    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
+    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
+                                         @RequestParam("toType") String strToType,
+                                         @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("toId", strToId);
         checkParameter("toType", strToType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByTo(entityId).get());
+            return checkNotNull(relationService.findByTo(entityId, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {"toId", "toType"})
+    @ResponseBody
+    public List<EntityRelationInfo> findInfoByTo(@RequestParam("toId") String strToId,
+                                                   @RequestParam("toType") String strToType,
+                                                   @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
+        checkParameter("toId", strToId);
+        checkParameter("toType", strToType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
+        checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
+        try {
+            return checkNotNull(relationService.findInfoByTo(entityId, typeGroup).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {"toId", "toType", "relationType"})
     @ResponseBody
-    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId, @RequestParam("toType") String strToType
-            , @RequestParam("relationType") String strRelationType) throws ThingsboardException {
+    public List<EntityRelation> findByTo(@RequestParam("toId") String strToId,
+                                         @RequestParam("toType") String strToType,
+                                         @RequestParam("relationType") String strRelationType,
+                                         @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException {
         checkParameter("toId", strToId);
         checkParameter("toType", strToType);
         checkParameter("relationType", strRelationType);
         EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
         checkEntityId(entityId);
+        RelationTypeGroup typeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
         try {
-            return checkNotNull(relationService.findByToAndType(entityId, strRelationType).get());
+            return checkNotNull(relationService.findByToAndType(entityId, strRelationType, typeGroup).get());
         } catch (Exception e) {
             throw handleException(e);
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/relations", method = RequestMethod.POST)
     @ResponseBody
     public List<EntityRelation> findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {
@@ -191,4 +250,16 @@ public class EntityRelationController extends BaseController {
         }
     }
 
+    private RelationTypeGroup parseRelationTypeGroup(String strRelationTypeGroup, RelationTypeGroup defaultValue) {
+        RelationTypeGroup result = defaultValue;
+        if (strRelationTypeGroup != null && strRelationTypeGroup.trim().length()>0) {
+            try {
+                result = RelationTypeGroup.valueOf(strRelationTypeGroup);
+            } catch (IllegalArgumentException e) {
+                result = defaultValue;
+            }
+        }
+        return result;
+    }
+
 }
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 69b1cd7..4b488af 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -19,12 +19,18 @@ server:
   address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
   # Server bind port
   port: "${HTTP_BIND_PORT:8080}"
-# Uncomment the following section to enable ssl
-#  ssl:
-#    key-store: classpath:keystore/keystore.p12
-#    key-store-password: thingsboard
-#    keyStoreType: PKCS12
-#    keyAlias: tomcat
+  # Server SSL configuration
+  ssl:
+    # Enable/disable SSL support
+    enabled: "${SSL_ENABLED:false}"
+    # Path to the key store that holds the SSL certificate
+    key-store: "${SSL_KEY_STORE:classpath:keystore/keystore.p12}"
+    # Password used to access the key store
+    key-store-password: "${SSL_KEY_STORE_PASSWORD:thingsboard}"
+    # Type of the key store
+    key-store-type: "${SSL_KEY_STORE_TYPE:PKCS12}"
+    # Alias that identifies the key in the key store
+    key-alias: "${SSL_KEY_ALIAS:tomcat}"
 
 # Zookeeper connection parameters. Used for service discovery.
 zk:
@@ -79,12 +85,18 @@ mqtt:
     leak_detector_level: "${NETTY_LEASK_DETECTOR_LVL:DISABLED}"
     boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
     worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
-# Uncomment the following lines to enable ssl for MQTT
-#  ssl:
-#    key_store: mqttserver.jks
-#    key_store_password: server_ks_password
-#    key_password: server_key_password
-#    key_store_type: JKS
+  # MQTT SSL configuration
+  ssl:
+    # Enable/disable SSL support
+    enabled: "${MQTT_SSL_ENABLED:false}"
+    # Path to the key store that holds the SSL certificate
+    key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
+    # Password used to access the key store
+    key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
+    # Password used to access the key
+    key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
+    # Type of the key store
+    key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
 
 # CoAP server parameters
 coap:
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 b9647e5..435f9a6 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -98,13 +98,15 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
 @IntegrationTest("server.port:0")
 public abstract class AbstractControllerTest {
 
+    protected static final String TEST_TENANT_NAME = "TEST TENANT";
+
     protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
     private static final String SYS_ADMIN_PASSWORD = "sysadmin";
     
-    protected static final String TENANT_ADMIN_EMAIL = "tenant@thingsboard.org";
+    protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org";
     private static final String TENANT_ADMIN_PASSWORD = "tenant";
 
-    protected static final String CUSTOMER_USER_EMAIL = "customer@thingsboard.org";
+    protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";
     private static final String CUSTOMER_USER_PASSWORD = "customer";
     
     protected MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
@@ -147,7 +149,7 @@ public abstract class AbstractControllerTest {
         loginSysAdmin();
 
         Tenant tenant = new Tenant();
-        tenant.setTitle("Tenant");
+        tenant.setTitle(TEST_TENANT_NAME);
         Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
         Assert.assertNotNull(savedTenant);
         tenantId = savedTenant.getId();
diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
new file mode 100644
index 0000000..10a2ff7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
@@ -0,0 +1,659 @@
+/**
+ * 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.controller;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class AssetControllerTest extends AbstractControllerTest {
+
+    private IdComparator<Asset> idComparator = new IdComparator<>();
+
+    private Tenant savedTenant;
+    private User tenantAdmin;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSaveAsset() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        Assert.assertNotNull(savedAsset);
+        Assert.assertNotNull(savedAsset.getId());
+        Assert.assertTrue(savedAsset.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedAsset.getTenantId());
+        Assert.assertNotNull(savedAsset.getCustomerId());
+        Assert.assertEquals(NULL_UUID, savedAsset.getCustomerId().getId());
+        Assert.assertEquals(asset.getName(), savedAsset.getName());
+
+        savedAsset.setName("My new asset");
+        doPost("/api/asset", savedAsset, Asset.class);
+
+        Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(foundAsset.getName(), savedAsset.getName());
+    }
+
+    @Test
+    public void testFindAssetById() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+        Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertNotNull(foundAsset);
+        Assert.assertEquals(savedAsset, foundAsset);
+    }
+
+    @Test
+    public void testFindAssetTypesByTenantId() throws Exception {
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<3;i++) {
+            Asset asset = new Asset();
+            asset.setName("My asset B"+i);
+            asset.setType("typeB");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        for (int i=0;i<7;i++) {
+            Asset asset = new Asset();
+            asset.setName("My asset C"+i);
+            asset.setType("typeC");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        for (int i=0;i<9;i++) {
+            Asset asset = new Asset();
+            asset.setName("My asset A"+i);
+            asset.setType("typeA");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        List<TenantAssetType> assetTypes = doGetTyped("/api/asset/types",
+                new TypeReference<List<TenantAssetType>>(){});
+
+        Assert.assertNotNull(assetTypes);
+        Assert.assertEquals(3, assetTypes.size());
+        Assert.assertEquals("typeA", assetTypes.get(0).getType());
+        Assert.assertEquals("typeB", assetTypes.get(1).getType());
+        Assert.assertEquals("typeC", assetTypes.get(2).getType());
+    }
+
+    @Test
+    public void testDeleteAsset() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        doDelete("/api/asset/"+savedAsset.getId().getId().toString())
+                .andExpect(status().isOk());
+
+        doGet("/api/asset/"+savedAsset.getId().getId().toString())
+                .andExpect(status().isNotFound());
+    }
+
+    @Test
+    public void testSaveAssetWithEmptyType() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        doPost("/api/asset", asset)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Asset type should be specified")));
+    }
+
+    @Test
+    public void testSaveAssetWithEmptyName() throws Exception {
+        Asset asset = new Asset();
+        asset.setType("default");
+        doPost("/api/asset", asset)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Asset name should be specified")));
+    }
+
+    @Test
+    public void testAssignUnassignAssetToCustomer() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        Asset assignedAsset = doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+                + "/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(savedCustomer.getId(), assignedAsset.getCustomerId());
+
+        Asset foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(savedCustomer.getId(), foundAsset.getCustomerId());
+
+        Asset unassignedAsset =
+                doDelete("/api/customer/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, unassignedAsset.getCustomerId().getId());
+
+        foundAsset = doGet("/api/asset/" + savedAsset.getId().getId().toString(), Asset.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, foundAsset.getCustomerId().getId());
+    }
+
+    @Test
+    public void testAssignAssetToNonExistentCustomer() throws Exception {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        doPost("/api/customer/" + UUIDs.timeBased().toString()
+                + "/asset/" + savedAsset.getId().getId().toString())
+                .andExpect(status().isNotFound());
+    }
+
+    @Test
+    public void testAssignAssetToCustomerFromDifferentTenant() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant2 = new Tenant();
+        tenant2.setTitle("Different tenant");
+        Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class);
+        Assert.assertNotNull(savedTenant2);
+
+        User tenantAdmin2 = new User();
+        tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin2.setTenantId(savedTenant2.getId());
+        tenantAdmin2.setEmail("tenant3@thingsboard.org");
+        tenantAdmin2.setFirstName("Joe");
+        tenantAdmin2.setLastName("Downs");
+
+        tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1");
+
+        Customer customer = new Customer();
+        customer.setTitle("Different customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        login(tenantAdmin.getEmail(), "testPassword1");
+
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = doPost("/api/asset", asset, Asset.class);
+
+        doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+                + "/asset/" + savedAsset.getId().getId().toString())
+                .andExpect(status().isForbidden());
+
+        loginSysAdmin();
+
+        doDelete("/api/tenant/"+savedTenant2.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testFindTenantAssets() throws Exception {
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<178;i++) {
+            Asset asset = new Asset();
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            assets.add(doPost("/api/asset", asset, Asset.class));
+        }
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+    }
+
+    @Test
+    public void testFindTenantAssetsByName() throws Exception {
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle1.add(doPost("/api/asset", asset, Asset.class));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<75;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle2.add(doPost("/api/asset", asset, Asset.class));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindTenantAssetsByType() throws Exception {
+        String title1 = "Asset title 1";
+        String type1 = "typeA";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            assetsType1.add(doPost("/api/asset", asset, Asset.class));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeB";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<75;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            assetsType2.add(doPost("/api/asset", asset, Asset.class));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            doDelete("/api/asset/"+asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindCustomerAssets() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<128;i++) {
+            Asset asset = new Asset();
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            asset = doPost("/api/asset", asset, Asset.class);
+            assets.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+    }
+
+    @Test
+    public void testFindCustomerAssetsByName() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsTitle1.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsTitle2.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindCustomerAssetsByType() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        String type1 = "typeC";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsType1.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeD";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            asset = doPost("/api/asset", asset, Asset.class);
+            assetsType2.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/asset/" + asset.getId().getId().toString(), Asset.class));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                    new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            doDelete("/api/customer/asset/" + asset.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/assets?type={type}&",
+                new TypeReference<TextPageData<Asset>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
index 69bb761..5d40a79 100644
--- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
@@ -24,10 +24,7 @@ import java.util.Collections;
 import java.util.List;
 
 import org.apache.commons.lang3.RandomStringUtils;
-import org.thingsboard.server.common.data.Customer;
-import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.Tenant;
-import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.*;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceCredentialsId;
 import org.thingsboard.server.common.data.id.DeviceId;
@@ -83,6 +80,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDevice() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         Assert.assertNotNull(savedDevice);
@@ -114,16 +112,49 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testFindDeviceById() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
         Assert.assertNotNull(foundDevice);
         Assert.assertEquals(savedDevice, foundDevice);
     }
+
+    @Test
+    public void testFindDeviceTypesByTenantId() throws Exception {
+        List<Device> devices = new ArrayList<>();
+        for (int i=0;i<3;i++) {
+            Device device = new Device();
+            device.setName("My device B"+i);
+            device.setType("typeB");
+            devices.add(doPost("/api/device", device, Device.class));
+        }
+        for (int i=0;i<7;i++) {
+            Device device = new Device();
+            device.setName("My device C"+i);
+            device.setType("typeC");
+            devices.add(doPost("/api/device", device, Device.class));
+        }
+        for (int i=0;i<9;i++) {
+            Device device = new Device();
+            device.setName("My device A"+i);
+            device.setType("typeA");
+            devices.add(doPost("/api/device", device, Device.class));
+        }
+        List<TenantDeviceType> deviceTypes = doGetTyped("/api/device/types",
+                new TypeReference<List<TenantDeviceType>>(){});
+
+        Assert.assertNotNull(deviceTypes);
+        Assert.assertEquals(3, deviceTypes.size());
+        Assert.assertEquals("typeA", deviceTypes.get(0).getType());
+        Assert.assertEquals("typeB", deviceTypes.get(1).getType());
+        Assert.assertEquals("typeC", deviceTypes.get(2).getType());
+    }
     
     @Test
     public void testDeleteDevice() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         doDelete("/api/device/"+savedDevice.getId().getId().toString())
@@ -132,10 +163,20 @@ public class DeviceControllerTest extends AbstractControllerTest {
         doGet("/api/device/"+savedDevice.getId().getId().toString())
         .andExpect(status().isNotFound());
     }
-    
+
+    @Test
+    public void testSaveDeviceWithEmptyType() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        doPost("/api/device", device)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Device type should be specified")));
+    }
+
     @Test
     public void testSaveDeviceWithEmptyName() throws Exception {
         Device device = new Device();
+        device.setType("default");
         doPost("/api/device", device)
         .andExpect(status().isBadRequest())
         .andExpect(statusReason(containsString("Device name should be specified")));
@@ -145,6 +186,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testAssignUnassignDeviceToCustomer() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         Customer customer = new Customer();
@@ -170,6 +212,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testAssignDeviceToNonExistentCustomer() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         doPost("/api/customer/" + UUIDs.timeBased().toString() 
@@ -203,6 +246,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
         
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         
         doPost("/api/customer/" + savedCustomer.getId().getId().toString()
@@ -219,6 +263,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testFindDeviceCredentialsByDeviceId() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); 
@@ -229,6 +274,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentials() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); 
@@ -255,6 +301,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsType() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -268,6 +315,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsId() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -281,6 +329,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveNonExistentDeviceCredentials() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -298,6 +347,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
     public void testSaveDeviceCredentialsWithNonExistentDevice() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -307,9 +357,10 @@ public class DeviceControllerTest extends AbstractControllerTest {
     }
     
     @Test
-    public void testSaveDeviceCredentialsWithInvalidCredemtialsIdLength() throws Exception {
+    public void testSaveDeviceCredentialsWithInvalidCredentialsIdLength() throws Exception {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = doPost("/api/device", device, Device.class);
         DeviceCredentials deviceCredentials = 
                 doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
@@ -325,6 +376,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
         for (int i=0;i<178;i++) {
             Device device = new Device();
             device.setName("Device"+i);
+            device.setType("default");
             devices.add(doPost("/api/device", device, Device.class));
         }
         List<Device> loadedDevices = new ArrayList<>();
@@ -355,6 +407,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle1.add(doPost("/api/device", device, Device.class));
         }
         String title2 = "Device title 2";
@@ -365,6 +418,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle2.add(doPost("/api/device", device, Device.class));
         }
         
@@ -423,6 +477,89 @@ public class DeviceControllerTest extends AbstractControllerTest {
         Assert.assertFalse(pageData.hasNext());
         Assert.assertEquals(0, pageData.getData().size());
     }
+
+    @Test
+    public void testFindTenantDevicesByType() throws Exception {
+        String title1 = "Device title 1";
+        String type1 = "typeA";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            devicesType1.add(doPost("/api/device", device, Device.class));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeB";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<75;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            devicesType2.add(doPost("/api/device", device, Device.class));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            doDelete("/api/device/"+device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            doDelete("/api/device/"+device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/tenant/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
     
     @Test
     public void testFindCustomerDevices() throws Exception {
@@ -435,6 +572,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
         for (int i=0;i<128;i++) {
             Device device = new Device();
             device.setName("Device"+i);
+            device.setType("default");
             device = doPost("/api/device", device, Device.class);
             devices.add(doPost("/api/customer/" + customerId.getId().toString() 
                             + "/device/" + device.getId().getId().toString(), Device.class));
@@ -473,6 +611,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = doPost("/api/device", device, Device.class);
             devicesTitle1.add(doPost("/api/customer/" + customerId.getId().toString() 
                     + "/device/" + device.getId().getId().toString(), Device.class));
@@ -485,6 +624,7 @@ public class DeviceControllerTest extends AbstractControllerTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = doPost("/api/device", device, Device.class);
             devicesTitle2.add(doPost("/api/customer/" + customerId.getId().toString() 
                     + "/device/" + device.getId().getId().toString(), Device.class));
@@ -546,4 +686,96 @@ public class DeviceControllerTest extends AbstractControllerTest {
         Assert.assertEquals(0, pageData.getData().size());
     }
 
+    @Test
+    public void testFindCustomerDevicesByType() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Device title 1";
+        String type1 = "typeC";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            device = doPost("/api/device", device, Device.class);
+            devicesType1.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/device/" + device.getId().getId().toString(), Device.class));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeD";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            device = doPost("/api/device", device, Device.class);
+            devicesType2.add(doPost("/api/customer/" + customerId.getId().toString()
+                    + "/device/" + device.getId().getId().toString(), Device.class));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                    new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            doDelete("/api/customer/device/" + device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type1);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            doDelete("/api/customer/device/" + device.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?type={type}&",
+                new TypeReference<TextPageData<Device>>(){}, pageLink, type2);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
 }
diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
index 5d72076..dc4a422 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
@@ -130,7 +130,7 @@ public class TenantControllerTest extends AbstractControllerTest {
         Assert.assertEquals(tenants, loadedTenants);
         
         for (Tenant tenant : loadedTenants) {
-            if (!tenant.getTitle().equals("Tenant")) {
+            if (!tenant.getTitle().equals(TEST_TENANT_NAME)) {
                 doDelete("/api/tenant/"+tenant.getId().getId().toString())
                 .andExpect(status().isOk());        
             }
diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
index 7c00049..4fafd61 100644
--- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
@@ -182,7 +182,7 @@ public class UserControllerTest extends AbstractControllerTest {
         Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
         Assert.assertNotNull(savedTenant);
         
-        String email = "tenant@thingsboard.org";
+        String email = TENANT_ADMIN_EMAIL;
         User user = new User();
         user.setAuthority(Authority.TENANT_ADMIN);
         user.setTenantId(savedTenant.getId());
diff --git a/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
index b65a5a6..af8b50f 100644
--- a/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
@@ -47,6 +47,7 @@ public class HttpDeviceApiTest extends AbstractControllerTest {
         loginTenantAdmin();
         device = new Device();
         device.setName("My device");
+        device.setType("default");
         device = doPost("/api/device", device, Device.class);
 
         deviceCredentials =
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 19d671c..09695e5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -15,26 +15,47 @@
  */
 package org.thingsboard.server.common.data.alarm;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.JsonNode;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Data;
 import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
 
 /**
  * Created by ashvayka on 11.05.17.
  */
 @Data
-public class Alarm extends BaseData<AlarmId> {
+@Builder
+@AllArgsConstructor
+public class Alarm extends BaseData<AlarmId> implements HasName {
 
-    private long startTs;
-    private long endTs;
-    private long ackTs;
-    private long clearTs;
+    private TenantId tenantId;
     private String type;
     private EntityId originator;
     private AlarmSeverity severity;
     private AlarmStatus status;
+    private long startTs;
+    private long endTs;
+    private long ackTs;
+    private long clearTs;
     private JsonNode details;
     private boolean propagate;
 
+    public Alarm() {
+        super();
+    }
+
+    public Alarm(AlarmId id) {
+        super(id);
+    }
+
+    @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    public String getName() {
+        return type;
+    }
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
index 04d23a5..00ca6c3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
@@ -15,14 +15,19 @@
  */
 package org.thingsboard.server.common.data.alarm;
 
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Data;
 import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TimePageLink;
 
 /**
  * Created by ashvayka on 11.05.17.
  */
 @Data
+@Builder
+@AllArgsConstructor
 public class AlarmQuery {
 
     private EntityId affectedEntityId;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java
index 54fbc9d..0f1b346 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java
@@ -22,4 +22,12 @@ public enum AlarmStatus {
 
     ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK;
 
+    public boolean isAck() {
+        return this == ACTIVE_ACK || this == CLEARED_ACK;
+    }
+
+    public boolean isCleared() {
+        return this == CLEARED_ACK || this == CLEARED_UNACK;
+    }
+
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
index 2e20b77..dafa514 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -16,12 +16,13 @@
 package org.thingsboard.server.common.data.asset;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.SearchTextBased;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 
-public class Asset extends SearchTextBased<AssetId> {
+public class Asset extends SearchTextBased<AssetId> implements HasName {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
@@ -64,6 +65,7 @@ public class Asset extends SearchTextBased<AssetId> {
         this.customerId = customerId;
     }
 
+    @Override
     public String getName() {
         return name;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/TenantAssetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/TenantAssetType.java
new file mode 100644
index 0000000..bd23f2a
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/TenantAssetType.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.common.data.asset;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+public class TenantAssetType {
+
+    private static final long serialVersionUID = 8057290243855622101L;
+
+    private String type;
+    private TenantId tenantId;
+
+    public TenantAssetType() {
+        super();
+    }
+
+    public TenantAssetType(String type, TenantId tenantId) {
+        this.type = type;
+        this.tenantId = tenantId;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantAssetType that = (TenantAssetType) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantAssetType{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
index 71a4527..ec535bf 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -15,12 +15,14 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonProperty.Access;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Customer extends ContactBased<CustomerId>{
+public class Customer extends ContactBased<CustomerId> implements HasName {
     
     private static final long serialVersionUID = -1599722990298929275L;
     
@@ -59,6 +61,12 @@ public class Customer extends ContactBased<CustomerId>{
         this.title = title;
     }
 
+    @Override
+    @JsonProperty(access = Access.READ_ONLY)
+    public String getName() {
+        return title;
+    }
+
     public JsonNode getAdditionalInfo() {
         return additionalInfo;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
index d88df16..3be3b79 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
@@ -15,11 +15,12 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DashboardId;
 import org.thingsboard.server.common.data.id.TenantId;
 
-public class DashboardInfo extends SearchTextBased<DashboardId> {
+public class DashboardInfo extends SearchTextBased<DashboardId> implements HasName {
 
     private TenantId tenantId;
     private CustomerId customerId;
@@ -65,6 +66,12 @@ public class DashboardInfo extends SearchTextBased<DashboardId> {
     }
 
     @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    public String getName() {
+        return title;
+    }
+
+    @Override
     public String getSearchText() {
         return title;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
index 92e8655..7d3d4f5 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Device extends SearchTextBased<DeviceId> {
+public class Device extends SearchTextBased<DeviceId> implements HasName {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
@@ -64,6 +64,7 @@ public class Device extends SearchTextBased<DeviceId> {
         this.customerId = customerId;
     }
 
+    @Override
     public String getName() {
         return name;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index 21093ce..1f77904 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.common.data.id;
 
 import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.alarm.AlarmId;
 
 import java.util.UUID;
 
@@ -50,6 +51,8 @@ public class EntityIdFactory {
                 return new DeviceId(uuid);
             case ASSET:
                 return new AssetId(uuid);
+            case ALARM:
+                return new AlarmId(uuid);
         }
         throw new IllegalArgumentException("EntityType " + type + " is not supported!");
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
index 5019cd1..e5eb149 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
@@ -15,13 +15,14 @@
  */
 package org.thingsboard.server.common.data.plugin;
 
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.SearchTextBased;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class PluginMetaData extends SearchTextBased<PluginId> {
+public class PluginMetaData extends SearchTextBased<PluginId> implements HasName {
 
     private static final long serialVersionUID = 1L;
 
@@ -75,6 +76,7 @@ public class PluginMetaData extends SearchTextBased<PluginId> {
         this.tenantId = tenantId;
     }
 
+    @Override
     public String getName() {
         return name;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
index 8fcf269..8dab589 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
@@ -30,6 +30,7 @@ public class EntityRelation {
     private EntityId from;
     private EntityId to;
     private String type;
+    private RelationTypeGroup typeGroup;
     private JsonNode additionalInfo;
 
     public EntityRelation() {
@@ -37,21 +38,27 @@ public class EntityRelation {
     }
 
     public EntityRelation(EntityId from, EntityId to, String type) {
-        this(from, to, type, null);
+        this(from, to, type, RelationTypeGroup.COMMON);
     }
 
-    public EntityRelation(EntityId from, EntityId to, String type, JsonNode additionalInfo) {
+    public EntityRelation(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) {
+        this(from, to, type, typeGroup, null);
+    }
+
+    public EntityRelation(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup, JsonNode additionalInfo) {
         this.from = from;
         this.to = to;
         this.type = type;
+        this.typeGroup = typeGroup;
         this.additionalInfo = additionalInfo;
     }
 
-    public EntityRelation(EntityRelation device) {
-        this.from = device.getFrom();
-        this.to = device.getTo();
-        this.type = device.getType();
-        this.additionalInfo = device.getAdditionalInfo();
+    public EntityRelation(EntityRelation entityRelation) {
+        this.from = entityRelation.getFrom();
+        this.to = entityRelation.getTo();
+        this.type = entityRelation.getType();
+        this.typeGroup = entityRelation.getTypeGroup();
+        this.additionalInfo = entityRelation.getAdditionalInfo();
     }
 
     public EntityId getFrom() {
@@ -78,6 +85,14 @@ public class EntityRelation {
         this.type = type;
     }
 
+    public RelationTypeGroup getTypeGroup() {
+        return typeGroup;
+    }
+
+    public void setTypeGroup(RelationTypeGroup typeGroup) {
+        this.typeGroup = typeGroup;
+    }
+
     public JsonNode getAdditionalInfo() {
         return additionalInfo;
     }
@@ -90,14 +105,22 @@ public class EntityRelation {
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
-        EntityRelation relation = (EntityRelation) o;
-        return Objects.equals(from, relation.from) &&
-                Objects.equals(to, relation.to) &&
-                Objects.equals(type, relation.type);
+
+        EntityRelation that = (EntityRelation) o;
+
+        if (from != null ? !from.equals(that.from) : that.from != null) return false;
+        if (to != null ? !to.equals(that.to) : that.to != null) return false;
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return typeGroup == that.typeGroup;
+
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(from, to, type);
+        int result = from != null ? from.hashCode() : 0;
+        result = 31 * result + (to != null ? to.hashCode() : 0);
+        result = 31 * result + (type != null ? type.hashCode() : 0);
+        result = 31 * result + (typeGroup != null ? typeGroup.hashCode() : 0);
+        return result;
     }
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java
new file mode 100644
index 0000000..5012691
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java
@@ -0,0 +1,68 @@
+/**
+ * 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.common.data.relation;
+
+public class EntityRelationInfo extends EntityRelation {
+
+    private static final long serialVersionUID = 2807343097519543363L;
+
+    private String fromName;
+    private String toName;
+
+    public EntityRelationInfo() {
+        super();
+    }
+
+    public EntityRelationInfo(EntityRelation entityRelation) {
+        super(entityRelation);
+    }
+
+    public String getFromName() {
+        return fromName;
+    }
+
+    public void setFromName(String fromName) {
+        this.fromName = fromName;
+    }
+
+    public String getToName() {
+        return toName;
+    }
+
+    public void setToName(String toName) {
+        this.toName = toName;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+
+        EntityRelationInfo that = (EntityRelationInfo) o;
+
+        return toName != null ? toName.equals(that.toName) : that.toName == null;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (toName != null ? toName.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
new file mode 100644
index 0000000..82798ab
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java
@@ -0,0 +1,23 @@
+/**
+ * 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.common.data.relation;
+
+public enum RelationTypeGroup {
+
+    COMMON,
+    ALARM
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
index ecbc86c..8a0f847 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.rule;
 
 import lombok.Data;
 import lombok.ToString;
+import org.thingsboard.server.common.data.HasName;
 import org.thingsboard.server.common.data.SearchTextBased;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.RuleId;
@@ -26,7 +27,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
 
 @Data
-public class RuleMetaData extends SearchTextBased<RuleId> {
+public class RuleMetaData extends SearchTextBased<RuleId> implements HasName {
 
     private static final long serialVersionUID = -5656679015122935465L;
 
@@ -66,4 +67,9 @@ public class RuleMetaData extends SearchTextBased<RuleId> {
         return name;
     }
 
+    @Override
+    public String getName() {
+        return name;
+    }
+
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
index aee1510..4501759 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
@@ -15,11 +15,12 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Tenant extends ContactBased<TenantId>{
+public class Tenant extends ContactBased<TenantId> implements HasName {
 
     private static final long serialVersionUID = 8057243243859922101L;
     
@@ -50,6 +51,12 @@ public class Tenant extends ContactBased<TenantId>{
         this.title = title;
     }
 
+    @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    public String getName() {
+        return title;
+    }
+
     public String getRegion() {
         return region;
     }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantDeviceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantDeviceType.java
new file mode 100644
index 0000000..d611a25
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantDeviceType.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.common.data;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+public class TenantDeviceType {
+
+    private static final long serialVersionUID = 8057240243859922101L;
+
+    private String type;
+    private TenantId tenantId;
+
+    public TenantDeviceType() {
+        super();
+    }
+
+    public TenantDeviceType(String type, TenantId tenantId) {
+        this.type = type;
+        this.tenantId = tenantId;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantDeviceType that = (TenantDeviceType) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantDeviceType{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
index 74fa4cf..1b97e2e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
@@ -22,7 +23,7 @@ import org.thingsboard.server.common.data.security.Authority;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class User extends SearchTextBased<UserId> {
+public class User extends SearchTextBased<UserId> implements HasName {
 
     private static final long serialVersionUID = 8250339805336035966L;
 
@@ -77,6 +78,12 @@ public class User extends SearchTextBased<UserId> {
         this.email = email;
     }
 
+    @Override
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    public String getName() {
+        return email;
+    }
+
     public Authority getAuthority() {
         return authority;
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
index 01346b0..587d2b6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
@@ -127,7 +127,7 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
         log.debug("Save entity {}", entity);
         if (entity.getId() == null) {
             entity.setId(UUIDs.timeBased());
-        } else {
+        } else if (isDeleteOnSave()) {
             removeById(entity.getId());
         }
         Statement saveStatement = getSaveQuery(entity);
@@ -136,6 +136,10 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
         return new EntityResultSet<>(resultSet, entity);
     }
 
+    protected boolean isDeleteOnSave() {
+        return true;
+    }
+
     public T save(T entity) {
         return saveWithResult(entity).getEntity();
     }
@@ -161,9 +165,18 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
         return getSession().execute(delete);
     }
 
-
     public List<T> find() {
         log.debug("Get all entities from column family {}", getColumnFamilyName());
         return findListByStatement(QueryBuilder.select().all().from(getColumnFamilyName()).setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()));
     }
+
+    protected static <T> Function<BaseEntity<T>, T> toDataFunction() {
+        return new Function<BaseEntity<T>, T>() {
+            @Nullable
+            @Override
+            public T apply(@Nullable BaseEntity<T> entity) {
+                return entity != null ? entity.toData() : null;
+            }
+        };
+    }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTimeDao.java b/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTimeDao.java
index 5852e3c..8ccbb88 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTimeDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/AbstractSearchTimeDao.java
@@ -47,8 +47,27 @@ public abstract class AbstractSearchTimeDao<T extends BaseEntity<?>> extends Abs
         return findPageWithTimeSearch(searchView, clauses, Collections.singletonList(ordering), pageLink);
     }
 
-
     protected List<T> findPageWithTimeSearch(String searchView, List<Clause> clauses, List<Ordering> topLevelOrderings, TimePageLink pageLink) {
+        return findPageWithTimeSearch(searchView, clauses, topLevelOrderings, pageLink, ModelConstants.ID_PROPERTY);
+    }
+
+    protected List<T> findPageWithTimeSearch(String searchView, List<Clause> clauses, TimePageLink pageLink, String idColumn) {
+        return findPageWithTimeSearch(searchView, clauses, Collections.emptyList(), pageLink, idColumn);
+    }
+
+    protected List<T> findPageWithTimeSearch(String searchView, List<Clause> clauses, List<Ordering> topLevelOrderings, TimePageLink pageLink, String idColumn) {
+        return findListByStatement(buildQuery(searchView, clauses, topLevelOrderings, pageLink, idColumn));
+    }
+
+    public static Where buildQuery(String searchView, List<Clause> clauses, TimePageLink pageLink, String idColumn) {
+        return buildQuery(searchView, clauses, Collections.emptyList(), pageLink, idColumn);
+    }
+
+    public static Where buildQuery(String searchView, List<Clause> clauses, Ordering order, TimePageLink pageLink, String idColumn) {
+        return buildQuery(searchView, clauses, Collections.singletonList(order), pageLink, idColumn);
+    }
+
+    public static Where buildQuery(String searchView, List<Clause> clauses, List<Ordering> topLevelOrderings, TimePageLink pageLink, String idColumn) {
         Select select = select().from(searchView);
         Where query = select.where();
         for (Clause clause : clauses) {
@@ -57,34 +76,35 @@ public abstract class AbstractSearchTimeDao<T extends BaseEntity<?>> extends Abs
         query.limit(pageLink.getLimit());
         if (pageLink.isAscOrder()) {
             if (pageLink.getIdOffset() != null) {
-                query.and(QueryBuilder.gt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset()));
+                query.and(QueryBuilder.gt(idColumn, pageLink.getIdOffset()));
             } else if (pageLink.getStartTime() != null) {
                 final UUID startOf = UUIDs.startOf(pageLink.getStartTime());
-                query.and(QueryBuilder.gte(ModelConstants.ID_PROPERTY, startOf));
+                query.and(QueryBuilder.gte(idColumn, startOf));
             }
             if (pageLink.getEndTime() != null) {
                 final UUID endOf = UUIDs.endOf(pageLink.getEndTime());
-                query.and(QueryBuilder.lte(ModelConstants.ID_PROPERTY, endOf));
+                query.and(QueryBuilder.lte(idColumn, endOf));
             }
         } else {
             if (pageLink.getIdOffset() != null) {
-                query.and(QueryBuilder.lt(ModelConstants.ID_PROPERTY, pageLink.getIdOffset()));
+                query.and(QueryBuilder.lt(idColumn, pageLink.getIdOffset()));
             } else if (pageLink.getEndTime() != null) {
                 final UUID endOf = UUIDs.endOf(pageLink.getEndTime());
-                query.and(QueryBuilder.lte(ModelConstants.ID_PROPERTY, endOf));
+                query.and(QueryBuilder.lte(idColumn, endOf));
             }
             if (pageLink.getStartTime() != null) {
                 final UUID startOf = UUIDs.startOf(pageLink.getStartTime());
-                query.and(QueryBuilder.gte(ModelConstants.ID_PROPERTY, startOf));
+                query.and(QueryBuilder.gte(idColumn, startOf));
             }
         }
         List<Ordering> orderings = new ArrayList<>(topLevelOrderings);
         if (pageLink.isAscOrder()) {
-            orderings.add(QueryBuilder.asc(ModelConstants.ID_PROPERTY));
+            orderings.add(QueryBuilder.asc(idColumn));
         } else {
-            orderings.add(QueryBuilder.desc(ModelConstants.ID_PROPERTY));
+            orderings.add(QueryBuilder.desc(idColumn));
         }
         query.orderBy(orderings.toArray(new Ordering[orderings.size()]));
-        return findListByStatement(query);
+        return query;
     }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
index a116598..9fdd92e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
@@ -15,8 +15,27 @@
  */
 package org.thingsboard.server.dao.alarm;
 
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.Dao;
+import org.thingsboard.server.dao.model.AlarmEntity;
+
+import java.util.List;
+import java.util.UUID;
+
 /**
  * Created by ashvayka on 11.05.17.
  */
-public interface AlarmDao {
+public interface AlarmDao extends Dao<AlarmEntity> {
+
+    ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
+
+    ListenableFuture<Alarm> findAlarmByIdAsync(UUID key);
+
+    AlarmEntity save(Alarm alarm);
+
+    ListenableFuture<List<Alarm>> findAlarms(AlarmQuery query);
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java
new file mode 100644
index 0000000..1362e6b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java
@@ -0,0 +1,110 @@
+/**
+ * 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.dao.alarm;
+
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.AbstractModelDao;
+import org.thingsboard.server.dao.AbstractSearchTimeDao;
+import org.thingsboard.server.dao.model.AlarmEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.relation.RelationDao;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Component
+@Slf4j
+public class AlarmDaoImpl extends AbstractModelDao<AlarmEntity> implements AlarmDao {
+
+    @Autowired
+    private RelationDao relationDao;
+
+    @Override
+    protected Class<AlarmEntity> getColumnFamilyClass() {
+        return AlarmEntity.class;
+    }
+
+    @Override
+    protected String getColumnFamilyName() {
+        return ALARM_COLUMN_FAMILY_NAME;
+    }
+
+    protected boolean isDeleteOnSave() {
+        return false;
+    }
+
+    @Override
+    public AlarmEntity save(Alarm alarm) {
+        log.debug("Save asset [{}] ", alarm);
+        return save(new AlarmEntity(alarm));
+    }
+
+    @Override
+    public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
+        Select select = select().from(ALARM_COLUMN_FAMILY_NAME);
+        Select.Where query = select.where();
+        query.and(eq(ALARM_TENANT_ID_PROPERTY, tenantId.getId()));
+        query.and(eq(ALARM_ORIGINATOR_ID_PROPERTY, originator.getId()));
+        query.and(eq(ALARM_ORIGINATOR_TYPE_PROPERTY, originator.getEntityType()));
+        query.and(eq(ALARM_TYPE_PROPERTY, type));
+        query.limit(1);
+        query.orderBy(QueryBuilder.asc(ModelConstants.ALARM_TYPE_PROPERTY), QueryBuilder.desc(ModelConstants.ID_PROPERTY));
+        return Futures.transform(findOneByStatementAsync(query), toDataFunction());
+    }
+
+    @Override
+    public ListenableFuture<Alarm> findAlarmByIdAsync(UUID key) {
+        log.debug("Get alarm by id {}", key);
+        Select.Where query = select().from(ALARM_BY_ID_VIEW_NAME).where(eq(ModelConstants.ID_PROPERTY, key));
+        query.limit(1);
+        log.trace("Execute query {}", query);
+        return Futures.transform(findOneByStatementAsync(query), toDataFunction());
+    }
+
+    @Override
+    public ListenableFuture<List<Alarm>> findAlarms(AlarmQuery query) {
+        log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink());
+        EntityId affectedEntity = query.getAffectedEntityId();
+        String relationType = query.getStatus() == null ? BaseAlarmService.ALARM_RELATION : BaseAlarmService.ALARM_RELATION_PREFIX + query.getStatus().name();
+        ListenableFuture<List<EntityRelation>> relations = relationDao.findRelations(affectedEntity, relationType, RelationTypeGroup.ALARM, EntityType.ALARM, query.getPageLink());
+        return Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Alarm>>) input -> {
+            List<ListenableFuture<Alarm>> alarmFutures = new ArrayList<>(input.size());
+            for (EntityRelation relation : input) {
+                alarmFutures.add(findAlarmByIdAsync(relation.getTo().getId()));
+            }
+            return Futures.successfulAsList(alarmFutures);
+        });
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
index 35bd247..5399d9d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
@@ -21,20 +21,18 @@ import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.alarm.AlarmQuery;
 import org.thingsboard.server.common.data.page.TimePageData;
 
-import java.util.Optional;
-
 /**
  * Created by ashvayka on 11.05.17.
  */
 public interface AlarmService {
 
-    Optional<Alarm> saveIfNotExists(Alarm alarm);
+    Alarm createOrUpdateAlarm(Alarm alarm);
 
-    ListenableFuture<Boolean> updateAlarm(Alarm alarm);
+    ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTs);
 
-    ListenableFuture<Boolean> ackAlarm(Alarm alarm);
+    ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long ackTs);
 
-    ListenableFuture<Boolean> clearAlarm(AlarmId alarmId);
+    ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId);
 
     ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query);
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
new file mode 100644
index 0000000..3981f64
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -0,0 +1,292 @@
+/**
+ * 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.dao.alarm;
+
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
+import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.model.*;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.service.DataValidator;
+import org.thingsboard.server.dao.tenant.TenantDao;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.server.dao.DaoUtil.*;
+import static org.thingsboard.server.dao.service.Validator.*;
+
+@Service
+@Slf4j
+public class BaseAlarmService extends AbstractEntityService implements AlarmService {
+
+    public static final String ALARM_RELATION_PREFIX = "ALARM_";
+    public static final String ALARM_RELATION = "ALARM_ANY";
+
+    @Autowired
+    private AlarmDao alarmDao;
+
+    @Autowired
+    private TenantDao tenantDao;
+
+    @Autowired
+    private RelationService relationService;
+
+    protected ExecutorService readResultsProcessingExecutor;
+
+    @PostConstruct
+    public void startExecutor() {
+        readResultsProcessingExecutor = Executors.newCachedThreadPool();
+    }
+
+    @PreDestroy
+    public void stopExecutor() {
+        if (readResultsProcessingExecutor != null) {
+            readResultsProcessingExecutor.shutdownNow();
+        }
+    }
+
+    @Override
+    public Alarm createOrUpdateAlarm(Alarm alarm) {
+        alarmDataValidator.validate(alarm);
+        try {
+            if (alarm.getStartTs() == 0L) {
+                alarm.setStartTs(System.currentTimeMillis());
+            }
+            if (alarm.getEndTs() == 0L) {
+                alarm.setEndTs(alarm.getStartTs());
+            }
+            if (alarm.getId() == null) {
+                Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get();
+                if (existing == null || existing.getStatus().isCleared()) {
+                    return createAlarm(alarm);
+                } else {
+                    return updateAlarm(existing, alarm);
+                }
+            } else {
+                return updateAlarm(alarm).get();
+            }
+        } catch (ExecutionException | InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private Alarm createAlarm(Alarm alarm) throws InterruptedException, ExecutionException {
+        log.debug("New Alarm : {}", alarm);
+        Alarm saved = getData(alarmDao.save(new AlarmEntity(alarm)));
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(new RelationsSearchParameters(saved.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
+        List<EntityId> parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList());
+        for (EntityId parentId : parentEntities) {
+            createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION, RelationTypeGroup.ALARM));
+            createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name(), RelationTypeGroup.ALARM));
+        }
+        createRelation(new EntityRelation(alarm.getOriginator(), saved.getId(), ALARM_RELATION, RelationTypeGroup.ALARM));
+        createRelation(new EntityRelation(alarm.getOriginator(), saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name(), RelationTypeGroup.ALARM));
+        return saved;
+    }
+
+    protected ListenableFuture<Alarm> updateAlarm(Alarm update) {
+        alarmDataValidator.validate(update);
+        return getAndUpdate(update.getId(), new Function<Alarm, Alarm>() {
+            @Nullable
+            @Override
+            public Alarm apply(@Nullable Alarm alarm) {
+                if (alarm == null) {
+                    return null;
+                } else {
+                    return updateAlarm(alarm, update);
+                }
+            }
+        });
+    }
+
+    private Alarm updateAlarm(Alarm oldAlarm, Alarm newAlarm) {
+        AlarmStatus oldStatus = oldAlarm.getStatus();
+        AlarmStatus newStatus = newAlarm.getStatus();
+        AlarmEntity result = alarmDao.save(new AlarmEntity(merge(oldAlarm, newAlarm)));
+        if (oldStatus != newStatus) {
+            updateRelations(oldAlarm, oldStatus, newStatus);
+        }
+        return result.toData();
+    }
+
+    @Override
+    public ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTime) {
+        return getAndUpdate(alarmId, new Function<Alarm, Boolean>() {
+            @Nullable
+            @Override
+            public Boolean apply(@Nullable Alarm alarm) {
+                if (alarm == null || alarm.getStatus().isAck()) {
+                    return false;
+                } else {
+                    AlarmStatus oldStatus = alarm.getStatus();
+                    AlarmStatus newStatus = oldStatus.isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK;
+                    alarm.setStatus(newStatus);
+                    alarm.setAckTs(ackTime);
+                    alarmDao.save(new AlarmEntity(alarm));
+                    updateRelations(alarm, oldStatus, newStatus);
+                    return true;
+                }
+            }
+        });
+    }
+
+    @Override
+    public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long clearTime) {
+        return getAndUpdate(alarmId, new Function<Alarm, Boolean>() {
+            @Nullable
+            @Override
+            public Boolean apply(@Nullable Alarm alarm) {
+                if (alarm == null || alarm.getStatus().isCleared()) {
+                    return false;
+                } else {
+                    AlarmStatus oldStatus = alarm.getStatus();
+                    AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
+                    alarm.setStatus(newStatus);
+                    alarm.setClearTs(clearTime);
+                    alarmDao.save(new AlarmEntity(alarm));
+                    updateRelations(alarm, oldStatus, newStatus);
+                    return true;
+                }
+            }
+        });
+    }
+
+    @Override
+    public ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId) {
+        log.trace("Executing findAlarmById [{}]", alarmId);
+        validateId(alarmId, "Incorrect alarmId " + alarmId);
+        return alarmDao.findAlarmByIdAsync(alarmId.getId());
+    }
+
+    @Override
+    public ListenableFuture<TimePageData<Alarm>> findAlarms(AlarmQuery query) {
+        ListenableFuture<List<Alarm>> alarms = alarmDao.findAlarms(query);
+        return Futures.transform(alarms, new Function<List<Alarm>, TimePageData<Alarm>>() {
+            @Nullable
+            @Override
+            public TimePageData<Alarm> apply(@Nullable List<Alarm> alarms) {
+                return new TimePageData<>(alarms, query.getPageLink());
+            }
+        });
+    }
+
+    private void deleteRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
+        log.debug("Deleting Alarm relation: {}", alarmRelation);
+        relationService.deleteRelation(alarmRelation).get();
+    }
+
+    private void createRelation(EntityRelation alarmRelation) throws ExecutionException, InterruptedException {
+        log.debug("Creating Alarm relation: {}", alarmRelation);
+        relationService.saveRelation(alarmRelation).get();
+    }
+
+    private Alarm merge(Alarm existing, Alarm alarm) {
+        if (alarm.getStartTs() > existing.getEndTs()) {
+            existing.setEndTs(alarm.getStartTs());
+        }
+        if (alarm.getEndTs() > existing.getEndTs()) {
+            existing.setEndTs(alarm.getEndTs());
+        }
+        if (alarm.getClearTs() > existing.getClearTs()) {
+            existing.setClearTs(alarm.getClearTs());
+        }
+        if (alarm.getAckTs() > existing.getAckTs()) {
+            existing.setAckTs(alarm.getAckTs());
+        }
+        existing.setStatus(alarm.getStatus());
+        existing.setSeverity(alarm.getSeverity());
+        existing.setDetails(alarm.getDetails());
+        return existing;
+    }
+
+    private void updateRelations(Alarm alarm, AlarmStatus oldStatus, AlarmStatus newStatus) {
+        try {
+            EntityRelationsQuery query = new EntityRelationsQuery();
+            query.setParameters(new RelationsSearchParameters(alarm.getOriginator(), EntitySearchDirection.TO, Integer.MAX_VALUE));
+            List<EntityId> parentEntities = relationService.findByQuery(query).get().stream().map(r -> r.getFrom()).collect(Collectors.toList());
+            for (EntityId parentId : parentEntities) {
+                deleteRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name(), RelationTypeGroup.ALARM));
+                createRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name(), RelationTypeGroup.ALARM));
+            }
+            deleteRelation(new EntityRelation(alarm.getOriginator(), alarm.getId(), ALARM_RELATION_PREFIX + oldStatus.name(), RelationTypeGroup.ALARM));
+            createRelation(new EntityRelation(alarm.getOriginator(), alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name(), RelationTypeGroup.ALARM));
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("[{}] Failed to update relations. Old status: [{}], New status: [{}]", alarm.getId(), oldStatus, newStatus);
+            throw new RuntimeException(e);
+        }
+    }
+
+    private <T> ListenableFuture<T> getAndUpdate(AlarmId alarmId, Function<Alarm, T> function) {
+        validateId(alarmId, "Alarm id should be specified!");
+        ListenableFuture<Alarm> entity = alarmDao.findAlarmByIdAsync(alarmId.getId());
+        return Futures.transform(entity, function, readResultsProcessingExecutor);
+    }
+
+    private DataValidator<Alarm> alarmDataValidator =
+            new DataValidator<Alarm>() {
+
+                @Override
+                protected void validateDataImpl(Alarm alarm) {
+                    if (StringUtils.isEmpty(alarm.getType())) {
+                        throw new DataValidationException("Alarm type should be specified!");
+                    }
+                    if (alarm.getOriginator() == null) {
+                        throw new DataValidationException("Alarm originator should be specified!");
+                    }
+                    if (alarm.getSeverity() == null) {
+                        throw new DataValidationException("Alarm severity should be specified!");
+                    }
+                    if (alarm.getStatus() == null) {
+                        throw new DataValidationException("Alarm status should be specified!");
+                    }
+                    if (alarm.getTenantId() == null) {
+                        throw new DataValidationException("Alarm should be assigned to tenant!");
+                    } else {
+                        TenantEntity tenant = tenantDao.findById(alarm.getTenantId().getId());
+                        if (tenant == null) {
+                            throw new DataValidationException("Alarm is referencing to non-existent tenant!");
+                        }
+                    }
+                }
+            };
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
index 81f95b4..c2ffd1f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
@@ -16,12 +16,11 @@
 package org.thingsboard.server.dao.asset;
 
 import com.google.common.util.concurrent.ListenableFuture;
-import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.Dao;
 import org.thingsboard.server.dao.model.AssetEntity;
-import org.thingsboard.server.dao.model.DeviceEntity;
+import org.thingsboard.server.dao.model.TenantAssetTypeEntity;
 
 import java.util.List;
 import java.util.Optional;
@@ -51,6 +50,16 @@ public interface AssetDao extends Dao<AssetEntity> {
     List<AssetEntity> findAssetsByTenantId(UUID tenantId, TextPageLink pageLink);
 
     /**
+     * Find assets by tenantId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of asset objects
+     */
+    List<AssetEntity> findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink);
+
+    /**
      * Find assets by tenantId and assets Ids.
      *
      * @param tenantId the tenantId
@@ -70,6 +79,17 @@ public interface AssetDao extends Dao<AssetEntity> {
     List<AssetEntity> findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
 
     /**
+     * Find assets by tenantId, customerId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param customerId the customerId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of asset objects
+     */
+    List<AssetEntity> findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink);
+
+    /**
      * Find assets by tenantId, customerId and assets Ids.
      *
      * @param tenantId the tenantId
@@ -87,4 +107,12 @@ public interface AssetDao extends Dao<AssetEntity> {
      * @return the optional asset object
      */
     Optional<AssetEntity> findAssetsByTenantIdAndName(UUID tenantId, String name);
+
+    /**
+     * Find tenants asset types.
+     *
+     * @return the list of tenant asset type objects
+     */
+    ListenableFuture<List<TenantAssetTypeEntity>> findTenantAssetTypesAsync();
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java
index ed08c6d..5dba18b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java
@@ -15,7 +15,12 @@
  */
 package org.thingsboard.server.dao.asset;
 
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.mapping.Result;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
@@ -23,7 +28,9 @@ import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.AbstractSearchTextDao;
 import org.thingsboard.server.dao.model.AssetEntity;
+import org.thingsboard.server.dao.model.TenantAssetTypeEntity;
 
+import javax.annotation.Nullable;
 import java.util.*;
 
 import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
@@ -60,6 +67,16 @@ public class AssetDaoImpl extends AbstractSearchTextDao<AssetEntity> implements 
     }
 
     @Override
+    public List<AssetEntity> findAssetsByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
+        log.debug("Try to find assets by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink);
+        List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(ASSET_TYPE_PROPERTY, type),
+                        eq(ASSET_TENANT_ID_PROPERTY, tenantId)), pageLink);
+        log.trace("Found assets [{}] by tenantId [{}], type [{}] and pageLink [{}]", assetEntities, tenantId, type, pageLink);
+        return assetEntities;
+    }
+
+    @Override
     public ListenableFuture<List<AssetEntity>> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List<UUID> assetIds) {
         log.debug("Try to find assets by tenantId [{}] and asset Ids [{}]", tenantId, assetIds);
         Select select = select().from(getColumnFamilyName());
@@ -82,6 +99,19 @@ public class AssetDaoImpl extends AbstractSearchTextDao<AssetEntity> implements 
     }
 
     @Override
+    public List<AssetEntity> findAssetsByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
+        log.debug("Try to find assets by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", tenantId, customerId, type, pageLink);
+        List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(ASSET_TYPE_PROPERTY, type),
+                        eq(ASSET_CUSTOMER_ID_PROPERTY, customerId),
+                        eq(ASSET_TENANT_ID_PROPERTY, tenantId)),
+                pageLink);
+
+        log.trace("Found assets [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", assetEntities, tenantId, customerId, type, pageLink);
+        return assetEntities;
+    }
+
+    @Override
     public ListenableFuture<List<AssetEntity>> findAssetsByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> assetIds) {
         log.debug("Try to find assets by tenantId [{}], customerId [{}] and asset Ids [{}]", tenantId, customerId, assetIds);
         Select select = select().from(getColumnFamilyName());
@@ -101,4 +131,24 @@ public class AssetDaoImpl extends AbstractSearchTextDao<AssetEntity> implements 
         return Optional.ofNullable(findOneByStatement(query));
     }
 
+    @Override
+    public ListenableFuture<List<TenantAssetTypeEntity>> findTenantAssetTypesAsync() {
+        Select statement = select().distinct().column(ASSET_TYPE_PROPERTY).column(ASSET_TENANT_ID_PROPERTY).from(ASSET_TYPES_BY_TENANT_VIEW_NAME);
+        statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
+        ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+        ListenableFuture<List<TenantAssetTypeEntity>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<TenantAssetTypeEntity>>() {
+            @Nullable
+            @Override
+            public List<TenantAssetTypeEntity> apply(@Nullable ResultSet resultSet) {
+                Result<TenantAssetTypeEntity> result = cluster.getMapper(TenantAssetTypeEntity.class).map(resultSet);
+                if (result != null) {
+                    return result.all();
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        });
+        return result;
+    }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
index 7a61bd8..25baeda 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.asset;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -34,7 +35,7 @@ public interface AssetService {
 
     Optional<Asset> findAssetByTenantIdAndName(TenantId tenantId, String name);
 
-    Asset saveAsset(Asset device);
+    Asset saveAsset(Asset asset);
 
     Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId);
 
@@ -44,16 +45,21 @@ public interface AssetService {
 
     TextPageData<Asset> findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink);
 
+    TextPageData<Asset> findAssetsByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds);
 
     void deleteAssetsByTenantId(TenantId tenantId);
 
     TextPageData<Asset> findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
 
+    TextPageData<Asset> findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<AssetId> assetIds);
 
     void unassignCustomerAssets(TenantId tenantId, CustomerId customerId);
 
     ListenableFuture<List<Asset>> findAssetsByQuery(AssetSearchQuery query);
 
+    ListenableFuture<List<TenantAssetType>> findAssetTypesByTenantId(TenantId tenantId);
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
index c63e5d7..3a5f803 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
@@ -26,6 +26,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.EntityId;
@@ -34,12 +35,9 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.model.AssetEntity;
-import org.thingsboard.server.dao.model.CustomerEntity;
-import org.thingsboard.server.dao.model.TenantEntity;
-import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.model.*;
 import org.thingsboard.server.dao.relation.EntitySearchDirection;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
@@ -57,7 +55,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
 
 @Service
 @Slf4j
-public class BaseAssetService extends BaseEntityService implements AssetService {
+public class BaseAssetService extends AbstractEntityService implements AssetService {
 
     @Autowired
     private AssetDao assetDao;
@@ -132,7 +130,18 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
         validatePageLink(pageLink, "Incorrect page link " + pageLink);
         List<AssetEntity> assetEntities = assetDao.findAssetsByTenantId(tenantId.getId(), pageLink);
         List<Asset> assets = convertDataList(assetEntities);
-        return new TextPageData<Asset>(assets, pageLink);
+        return new TextPageData<>(assets, pageLink);
+    }
+
+    @Override
+    public TextPageData<Asset> findAssetsByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink) {
+        log.trace("Executing findAssetsByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<AssetEntity> assetEntities = assetDao.findAssetsByTenantIdAndType(tenantId.getId(), type, pageLink);
+        List<Asset> assets = convertDataList(assetEntities);
+        return new TextPageData<>(assets, pageLink);
     }
 
     @Override
@@ -159,7 +168,19 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
         validatePageLink(pageLink, "Incorrect page link " + pageLink);
         List<AssetEntity> assetEntities = assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
         List<Asset> assets = convertDataList(assetEntities);
-        return new TextPageData<Asset>(assets, pageLink);
+        return new TextPageData<>(assets, pageLink);
+    }
+
+    @Override
+    public TextPageData<Asset> findAssetsByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink) {
+        log.trace("Executing findAssetsByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<AssetEntity> assetEntities = assetDao.findAssetsByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
+        List<Asset> assets = convertDataList(assetEntities);
+        return new TextPageData<>(assets, pageLink);
     }
 
     @Override
@@ -207,6 +228,25 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
         return assets;
     }
 
+    @Override
+    public ListenableFuture<List<TenantAssetType>> findAssetTypesByTenantId(TenantId tenantId) {
+        log.trace("Executing findAssetTypesByTenantId, tenantId [{}]", tenantId);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        ListenableFuture<List<TenantAssetTypeEntity>> tenantAssetTypeEntities = assetDao.findTenantAssetTypesAsync();
+        ListenableFuture<List<TenantAssetType>> tenantAssetTypes = Futures.transform(tenantAssetTypeEntities,
+                (Function<List<TenantAssetTypeEntity>, List<TenantAssetType>>) assetTypeEntities -> {
+                    List<TenantAssetType> assetTypes = new ArrayList<>();
+                    for (TenantAssetTypeEntity assetTypeEntity : assetTypeEntities) {
+                        if (assetTypeEntity.getTenantId().equals(tenantId.getId())) {
+                            assetTypes.add(assetTypeEntity.toTenantAssetType());
+                        }
+                    }
+                    assetTypes.sort((TenantAssetType o1, TenantAssetType o2) -> o1.getType().compareTo(o2.getType()));
+                    return assetTypes;
+                });
+        return tenantAssetTypes;
+    }
+
     private DataValidator<Asset> assetValidator =
             new DataValidator<Asset>() {
 
@@ -232,6 +272,9 @@ public class BaseAssetService extends BaseEntityService implements AssetService 
 
                 @Override
                 protected void validateDataImpl(Asset asset) {
+                    if (StringUtils.isEmpty(asset.getType())) {
+                        throw new DataValidationException("Asset type should be specified!");
+                    }
                     if (StringUtils.isEmpty(asset.getName())) {
                         throw new DataValidationException("Asset name should be specified!");
                     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java
index b4fbc65..7c435bf 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java
@@ -45,7 +45,6 @@ public class ServiceCacheConfiguration {
     @Value("${cache.device_credentials.time_to_live}")
     private Integer cacheDeviceCredentialsTTL;
 
-
     @Value("${zk.enabled}")
     private boolean zkEnabled;
     @Value("${zk.url}")
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
index e9f6836..ff726da 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java
@@ -31,17 +31,15 @@ import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.thingsboard.server.common.data.Customer;
-import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.AssetEntity;
 import org.thingsboard.server.dao.model.CustomerEntity;
 import org.thingsboard.server.dao.model.TenantEntity;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -53,7 +51,7 @@ import org.springframework.stereotype.Service;
 import org.thingsboard.server.dao.service.Validator;
 @Service
 @Slf4j
-public class CustomerServiceImpl extends BaseEntityService implements CustomerService {
+public class CustomerServiceImpl extends AbstractEntityService implements CustomerService {
 
     private static final String PUBLIC_CUSTOMER_TITLE = "Public";
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
index b0ebbfd..f1f174e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.dashboard;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Dashboard;
 import org.thingsboard.server.common.data.DashboardInfo;
 import org.thingsboard.server.common.data.id.CustomerId;
@@ -27,8 +28,12 @@ public interface DashboardService {
     
     public Dashboard findDashboardById(DashboardId dashboardId);
 
+    public ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId);
+
     public DashboardInfo findDashboardInfoById(DashboardId dashboardId);
 
+    public ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId);
+
     public Dashboard saveDashboard(Dashboard dashboard);
     
     public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
index cf554f3..fc3e176 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
@@ -17,9 +17,13 @@ package org.thingsboard.server.dao.dashboard;
 
 import static org.thingsboard.server.dao.DaoUtil.convertDataList;
 import static org.thingsboard.server.dao.DaoUtil.getData;
+import static org.thingsboard.server.dao.service.Validator.validateId;
 
 import java.util.List;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.thingsboard.server.common.data.Dashboard;
@@ -30,7 +34,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.model.*;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -42,7 +46,7 @@ import org.thingsboard.server.dao.service.Validator;
 
 @Service
 @Slf4j
-public class DashboardServiceImpl extends BaseEntityService implements DashboardService {
+public class DashboardServiceImpl extends AbstractEntityService implements DashboardService {
 
     @Autowired
     private DashboardDao dashboardDao;
@@ -65,6 +69,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
     }
 
     @Override
+    public ListenableFuture<Dashboard> findDashboardByIdAsync(DashboardId dashboardId) {
+        log.trace("Executing findDashboardByIdAsync [{}]", dashboardId);
+        validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        ListenableFuture<DashboardEntity> dashboardEntity = dashboardDao.findByIdAsync(dashboardId.getId());
+        return Futures.transform(dashboardEntity, (Function<? super DashboardEntity, ? extends Dashboard>) input -> getData(input));
+    }
+
+    @Override
     public DashboardInfo findDashboardInfoById(DashboardId dashboardId) {
         log.trace("Executing findDashboardInfoById [{}]", dashboardId);
         Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
@@ -73,6 +85,14 @@ public class DashboardServiceImpl extends BaseEntityService implements Dashboard
     }
 
     @Override
+    public ListenableFuture<DashboardInfo> findDashboardInfoByIdAsync(DashboardId dashboardId) {
+        log.trace("Executing findDashboardInfoByIdAsync [{}]", dashboardId);
+        validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        ListenableFuture<DashboardInfoEntity> dashboardInfoEntity = dashboardInfoDao.findByIdAsync(dashboardId.getId());
+        return Futures.transform(dashboardInfoEntity, (Function<? super DashboardInfoEntity, ? extends DashboardInfo>) input -> getData(input));
+    }
+
+    @Override
     public Dashboard saveDashboard(Dashboard dashboard) {
         log.trace("Executing saveDashboard [{}]", dashboard);
         dashboardValidator.validate(dashboard);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
index b8d395c..241759c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.Dao;
 import org.thingsboard.server.dao.model.DeviceEntity;
+import org.thingsboard.server.dao.model.TenantDeviceTypeEntity;
 
 /**
  * The Interface DeviceDao.
@@ -49,6 +50,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
     List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink);
 
     /**
+     * Find devices by tenantId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of device objects
+     */
+    List<DeviceEntity> findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink);
+
+    /**
      * Find devices by tenantId and devices Ids.
      *
      * @param tenantId the tenantId
@@ -68,6 +79,18 @@ public interface DeviceDao extends Dao<DeviceEntity> {
     List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
 
     /**
+     * Find devices by tenantId, customerId, type and page link.
+     *
+     * @param tenantId the tenantId
+     * @param customerId the customerId
+     * @param type the type
+     * @param pageLink the page link
+     * @return the list of device objects
+     */
+    List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink);
+
+
+    /**
      * Find devices by tenantId, customerId and devices Ids.
      *
      * @param tenantId the tenantId
@@ -85,4 +108,11 @@ public interface DeviceDao extends Dao<DeviceEntity> {
      * @return the optional device object
      */
     Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String name);
+
+    /**
+     * Find tenants device types.
+     *
+     * @return the list of tenant device type objects
+     */
+    ListenableFuture<List<TenantDeviceTypeEntity>> findTenantDeviceTypesAsync();
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
index 81fc0bc..590f7b7 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
@@ -22,7 +22,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.*;
 
 import java.util.*;
 
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.querybuilder.Select;
+import com.datastax.driver.mapping.Result;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
@@ -32,6 +37,9 @@ import org.thingsboard.server.dao.AbstractSearchTextDao;
 import org.thingsboard.server.dao.model.DeviceEntity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.thingsboard.server.dao.model.TenantDeviceTypeEntity;
+
+import javax.annotation.Nullable;
 
 @Component
 @Slf4j
@@ -64,6 +72,16 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
     }
 
     @Override
+    public List<DeviceEntity> findDevicesByTenantIdAndType(UUID tenantId, String type, TextPageLink pageLink) {
+        log.debug("Try to find devices by tenantId [{}], type [{}] and pageLink [{}]", tenantId, type, pageLink);
+        List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type),
+                              eq(DEVICE_TENANT_ID_PROPERTY, tenantId)), pageLink);
+        log.trace("Found devices [{}] by tenantId [{}], type [{}] and pageLink [{}]", deviceEntities, tenantId, type, pageLink);
+        return deviceEntities;
+    }
+
+    @Override
     public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
         log.debug("Try to find devices by tenantId [{}] and device Ids [{}]", tenantId, deviceIds);
         Select select = select().from(getColumnFamilyName());
@@ -75,7 +93,7 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
 
     @Override
     public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
-        log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
+        log.debug("Try to find devices by tenantId [{}], customerId [{}] and pageLink [{}]", tenantId, customerId, pageLink);
         List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
                 Arrays.asList(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId),
                         eq(DEVICE_TENANT_ID_PROPERTY, tenantId)),
@@ -86,6 +104,19 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
     }
 
     @Override
+    public List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink) {
+        log.debug("Try to find devices by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", tenantId, customerId, type, pageLink);
+        List<DeviceEntity> deviceEntities = findPageWithTextSearch(DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(DEVICE_TYPE_PROPERTY, type),
+                              eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId),
+                              eq(DEVICE_TENANT_ID_PROPERTY, tenantId)),
+                pageLink);
+
+        log.trace("Found devices [{}] by tenantId [{}], customerId [{}], type [{}] and pageLink [{}]", deviceEntities, tenantId, customerId, type, pageLink);
+        return deviceEntities;
+    }
+
+    @Override
     public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
         log.debug("Try to find devices by tenantId [{}], customerId [{}] and device Ids [{}]", tenantId, customerId, deviceIds);
         Select select = select().from(getColumnFamilyName());
@@ -105,4 +136,24 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
         return Optional.ofNullable(findOneByStatement(query));
     }
 
+    @Override
+    public ListenableFuture<List<TenantDeviceTypeEntity>> findTenantDeviceTypesAsync() {
+        Select statement = select().distinct().column(DEVICE_TYPE_PROPERTY).column(DEVICE_TENANT_ID_PROPERTY).from(DEVICE_TYPES_BY_TENANT_VIEW_NAME);
+        statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
+        ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+        ListenableFuture<List<TenantDeviceTypeEntity>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<TenantDeviceTypeEntity>>() {
+            @Nullable
+            @Override
+            public List<TenantDeviceTypeEntity> apply(@Nullable ResultSet resultSet) {
+                Result<TenantDeviceTypeEntity> result = cluster.getMapper(TenantDeviceTypeEntity.class).map(resultSet);
+                if (result != null) {
+                    return result.all();
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        });
+        return result;
+    }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
index 4715435..3e7b6af 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.dao.device;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -44,16 +45,22 @@ public interface DeviceService {
 
     TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
 
+    TextPageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
 
     void deleteDevicesByTenantId(TenantId tenantId);
 
     TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
 
+    TextPageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink);
+
     ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
 
     void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
 
     ListenableFuture<List<Device>> findDevicesByQuery(DeviceSearchQuery query);
 
+    ListenableFuture<List<TenantDeviceType>> findDeviceTypesByTenantId(TenantId tenantId);
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
index ab6fa4e..99585ea 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
@@ -26,6 +26,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
@@ -36,10 +37,11 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.common.data.security.DeviceCredentialsType;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.model.CustomerEntity;
 import org.thingsboard.server.dao.model.DeviceEntity;
+import org.thingsboard.server.dao.model.TenantDeviceTypeEntity;
 import org.thingsboard.server.dao.model.TenantEntity;
 import org.thingsboard.server.dao.relation.EntitySearchDirection;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -47,9 +49,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.tenant.TenantDao;
 
 import javax.annotation.Nullable;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.dao.DaoUtil.*;
@@ -58,7 +58,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
 
 @Service
 @Slf4j
-public class DeviceServiceImpl extends BaseEntityService implements DeviceService {
+public class DeviceServiceImpl extends AbstractEntityService implements DeviceService {
 
     @Autowired
     private DeviceDao deviceDao;
@@ -148,7 +148,18 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
         validatePageLink(pageLink, "Incorrect page link " + pageLink);
         List<DeviceEntity> deviceEntities = deviceDao.findDevicesByTenantId(tenantId.getId(), pageLink);
         List<Device> devices = convertDataList(deviceEntities);
-        return new TextPageData<Device>(devices, pageLink);
+        return new TextPageData<>(devices, pageLink);
+    }
+
+    @Override
+    public TextPageData<Device> findDevicesByTenantIdAndType(TenantId tenantId, String type, TextPageLink pageLink) {
+        log.trace("Executing findDevicesByTenantIdAndType, tenantId [{}], type [{}], pageLink [{}]", tenantId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<DeviceEntity> deviceEntities = deviceDao.findDevicesByTenantIdAndType(tenantId.getId(), type, pageLink);
+        List<Device> devices = convertDataList(deviceEntities);
+        return new TextPageData<>(devices, pageLink);
     }
 
     @Override
@@ -176,7 +187,19 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
         validatePageLink(pageLink, "Incorrect page link " + pageLink);
         List<DeviceEntity> deviceEntities = deviceDao.findDevicesByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
         List<Device> devices = convertDataList(deviceEntities);
-        return new TextPageData<Device>(devices, pageLink);
+        return new TextPageData<>(devices, pageLink);
+    }
+
+    @Override
+    public TextPageData<Device> findDevicesByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, TextPageLink pageLink) {
+        log.trace("Executing findDevicesByTenantIdAndCustomerIdAndType, tenantId [{}], customerId [{}], type [{}], pageLink [{}]", tenantId, customerId, type, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        validateString(type, "Incorrect type " + type);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<DeviceEntity> deviceEntities = deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink);
+        List<Device> devices = convertDataList(deviceEntities);
+        return new TextPageData<>(devices, pageLink);
     }
 
     @Override
@@ -224,6 +247,25 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
         return devices;
     }
 
+    @Override
+    public ListenableFuture<List<TenantDeviceType>> findDeviceTypesByTenantId(TenantId tenantId) {
+        log.trace("Executing findDeviceTypesByTenantId, tenantId [{}]", tenantId);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        ListenableFuture<List<TenantDeviceTypeEntity>> tenantDeviceTypeEntities = deviceDao.findTenantDeviceTypesAsync();
+        ListenableFuture<List<TenantDeviceType>> tenantDeviceTypes = Futures.transform(tenantDeviceTypeEntities,
+            (Function<List<TenantDeviceTypeEntity>, List<TenantDeviceType>>) deviceTypeEntities -> {
+                List<TenantDeviceType> deviceTypes = new ArrayList<>();
+                for (TenantDeviceTypeEntity deviceTypeEntity : deviceTypeEntities) {
+                    if (deviceTypeEntity.getTenantId().equals(tenantId.getId())) {
+                        deviceTypes.add(deviceTypeEntity.toTenantDeviceType());
+                    }
+                }
+                deviceTypes.sort((TenantDeviceType o1, TenantDeviceType o2) -> o1.getType().compareTo(o2.getType()));
+                return deviceTypes;
+            });
+        return tenantDeviceTypes;
+    }
+
     private DataValidator<Device> deviceValidator =
             new DataValidator<Device>() {
 
@@ -249,6 +291,9 @@ public class DeviceServiceImpl extends BaseEntityService implements DeviceServic
 
                 @Override
                 protected void validateDataImpl(Device device) {
+                    if (StringUtils.isEmpty(device.getType())) {
+                        throw new DataValidationException("Device type should be specified!");
+                    }
                     if (StringUtils.isEmpty(device.getName())) {
                         throw new DataValidationException("Device name should be specified!");
                     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
new file mode 100644
index 0000000..ecca491
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
@@ -0,0 +1,36 @@
+/**
+ * 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.dao.entity;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.dao.relation.RelationService;
+
+@Slf4j
+public abstract class AbstractEntityService {
+
+    @Autowired
+    protected RelationService relationService;
+
+    protected void deleteEntityRelations(EntityId entityId) {
+        log.trace("Executing deleteEntityRelations [{}]", entityId);
+        relationService.deleteEntityRelations(entityId);
+    }
+
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
index 3c1c528..6f9500e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
@@ -15,23 +15,102 @@
  */
 package org.thingsboard.server.dao.entity;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.dao.relation.RelationService;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.user.UserService;
 
 /**
  * Created by ashvayka on 04.05.17.
  */
+@Service
 @Slf4j
-public class BaseEntityService {
+public class BaseEntityService extends AbstractEntityService implements EntityService {
 
     @Autowired
-    protected RelationService relationService;
+    private AssetService assetService;
 
-    protected void deleteEntityRelations(EntityId entityId) {
-        log.trace("Executing deleteEntityRelations [{}]", entityId);
-        relationService.deleteEntityRelations(entityId);
+    @Autowired
+    private DeviceService deviceService;
+
+    @Autowired
+    private RuleService ruleService;
+
+    @Autowired
+    private PluginService pluginService;
+
+    @Autowired
+    private TenantService tenantService;
+
+    @Autowired
+    private CustomerService customerService;
+
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private DashboardService dashboardService;
+
+    @Autowired
+    private AlarmService alarmService;
+
+    @Override
+    public void deleteEntityRelations(EntityId entityId) {
+        super.deleteEntityRelations(entityId);
+    }
+
+    @Override
+    public ListenableFuture<String> fetchEntityNameAsync(EntityId entityId) {
+        log.trace("Executing fetchEntityNameAsync [{}]", entityId);
+        ListenableFuture<String> entityName;
+        ListenableFuture<? extends HasName> hasName;
+        switch (entityId.getEntityType()) {
+            case ASSET:
+                hasName = assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
+                break;
+            case DEVICE:
+                hasName = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
+                break;
+            case RULE:
+                hasName = ruleService.findRuleByIdAsync(new RuleId(entityId.getId()));
+                break;
+            case PLUGIN:
+                hasName = pluginService.findPluginByIdAsync(new PluginId(entityId.getId()));
+                break;
+            case TENANT:
+                hasName = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+                break;
+            case CUSTOMER:
+                hasName = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
+                break;
+            case USER:
+                hasName = userService.findUserByIdAsync(new UserId(entityId.getId()));
+                break;
+            case DASHBOARD:
+                hasName = dashboardService.findDashboardInfoByIdAsync(new DashboardId(entityId.getId()));
+                break;
+            case ALARM:
+                hasName = alarmService.findAlarmByIdAsync(new AlarmId(entityId.getId()));
+                break;
+            default:
+                throw new IllegalStateException("Not Implemented!");
+        }
+        entityName = Futures.transform(hasName, (Function<HasName, String>) hasName1 -> hasName1.getName() );
+        return entityName;
     }
 
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
new file mode 100644
index 0000000..415f518
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityService.java
@@ -0,0 +1,28 @@
+/**
+ * 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.dao.entity;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.EntityId;
+
+public interface EntityService {
+
+    ListenableFuture<String> fetchEntityNameAsync(EntityId entityId);
+
+    void deleteEntityRelations(EntityId entityId);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/AlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/AlarmEntity.java
new file mode 100644
index 0000000..62cd08a
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/AlarmEntity.java
@@ -0,0 +1,236 @@
+/**
+ * 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.dao.model;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.datastax.driver.mapping.annotations.*;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.model.type.AlarmSeverityCodec;
+import org.thingsboard.server.dao.model.type.AlarmStatusCodec;
+import org.thingsboard.server.dao.model.type.EntityTypeCodec;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = ALARM_COLUMN_FAMILY_NAME)
+public final class AlarmEntity implements BaseEntity<Alarm> {
+
+    @Transient
+    private static final long serialVersionUID = -1265181166886910152L;
+
+    @ClusteringColumn(value = 1)
+    @Column(name = ID_PROPERTY)
+    private UUID id;
+
+    @PartitionKey(value = 0)
+    @Column(name = ALARM_TENANT_ID_PROPERTY)
+    private UUID tenantId;
+
+    @PartitionKey(value = 1)
+    @Column(name = ALARM_ORIGINATOR_ID_PROPERTY)
+    private UUID originatorId;
+
+    @PartitionKey(value = 2)
+    @Column(name = ALARM_ORIGINATOR_TYPE_PROPERTY, codec = EntityTypeCodec.class)
+    private EntityType originatorType;
+
+    @ClusteringColumn(value = 0)
+    @Column(name = ALARM_TYPE_PROPERTY)
+    private String type;
+
+    @Column(name = ALARM_SEVERITY_PROPERTY, codec = AlarmSeverityCodec.class)
+    private AlarmSeverity severity;
+
+    @Column(name = ALARM_STATUS_PROPERTY, codec = AlarmStatusCodec.class)
+    private AlarmStatus status;
+
+    @Column(name = ALARM_START_TS_PROPERTY)
+    private Long startTs;
+
+    @Column(name = ALARM_END_TS_PROPERTY)
+    private Long endTs;
+
+    @Column(name = ALARM_ACK_TS_PROPERTY)
+    private Long ackTs;
+
+    @Column(name = ALARM_CLEAR_TS_PROPERTY)
+    private Long clearTs;
+
+    @Column(name = ALARM_DETAILS_PROPERTY, codec = JsonCodec.class)
+    private JsonNode details;
+
+    @Column(name = ALARM_PROPAGATE_PROPERTY)
+    private Boolean propagate;
+
+    public AlarmEntity() {
+        super();
+    }
+
+    public AlarmEntity(Alarm alarm) {
+        if (alarm.getId() != null) {
+            this.id = alarm.getId().getId();
+        }
+        if (alarm.getTenantId() != null) {
+            this.tenantId = alarm.getTenantId().getId();
+        }
+        this.type = alarm.getType();
+        this.originatorId = alarm.getOriginator().getId();
+        this.originatorType = alarm.getOriginator().getEntityType();
+        this.type = alarm.getType();
+        this.severity = alarm.getSeverity();
+        this.status = alarm.getStatus();
+        this.propagate = alarm.isPropagate();
+        this.startTs = alarm.getStartTs();
+        this.endTs = alarm.getEndTs();
+        this.ackTs = alarm.getAckTs();
+        this.clearTs = alarm.getClearTs();
+        this.details = alarm.getDetails();
+    }
+
+    public UUID getId() {
+        return id;
+    }
+
+    public void setId(UUID id) {
+        this.id = id;
+    }
+
+    public UUID getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(UUID tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public UUID getOriginatorId() {
+        return originatorId;
+    }
+
+    public void setOriginatorId(UUID originatorId) {
+        this.originatorId = originatorId;
+    }
+
+    public EntityType getOriginatorType() {
+        return originatorType;
+    }
+
+    public void setOriginatorType(EntityType originatorType) {
+        this.originatorType = originatorType;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public AlarmSeverity getSeverity() {
+        return severity;
+    }
+
+    public void setSeverity(AlarmSeverity severity) {
+        this.severity = severity;
+    }
+
+    public AlarmStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(AlarmStatus status) {
+        this.status = status;
+    }
+
+    public Long getStartTs() {
+        return startTs;
+    }
+
+    public void setStartTs(Long startTs) {
+        this.startTs = startTs;
+    }
+
+    public Long getEndTs() {
+        return endTs;
+    }
+
+    public void setEndTs(Long endTs) {
+        this.endTs = endTs;
+    }
+
+    public Long getAckTs() {
+        return ackTs;
+    }
+
+    public void setAckTs(Long ackTs) {
+        this.ackTs = ackTs;
+    }
+
+    public Long getClearTs() {
+        return clearTs;
+    }
+
+    public void setClearTs(Long clearTs) {
+        this.clearTs = clearTs;
+    }
+
+    public JsonNode getDetails() {
+        return details;
+    }
+
+    public void setDetails(JsonNode details) {
+        this.details = details;
+    }
+
+    public Boolean getPropagate() {
+        return propagate;
+    }
+
+    public void setPropagate(Boolean propagate) {
+        this.propagate = propagate;
+    }
+
+    @Override
+    public Alarm toData() {
+        Alarm alarm = new Alarm(new AlarmId(id));
+        alarm.setCreatedTime(UUIDs.unixTimestamp(id));
+        if (tenantId != null) {
+            alarm.setTenantId(new TenantId(tenantId));
+        }
+        alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId));
+        alarm.setType(type);
+        alarm.setSeverity(severity);
+        alarm.setStatus(status);
+        alarm.setPropagate(propagate);
+        alarm.setStartTs(startTs);
+        alarm.setEndTs(endTs);
+        alarm.setAckTs(ackTs);
+        alarm.setClearTs(clearTs);
+        alarm.setDetails(details);
+        return alarm;
+    }
+
+}
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java
index 0444d11..0d477c0 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java
@@ -49,12 +49,13 @@ public final class AssetEntity implements SearchTextEntity<Asset> {
     @Column(name = ASSET_CUSTOMER_ID_PROPERTY)
     private UUID customerId;
 
-    @Column(name = ASSET_NAME_PROPERTY)
-    private String name;
-
+    @PartitionKey(value = 3)
     @Column(name = ASSET_TYPE_PROPERTY)
     private String type;
 
+    @Column(name = ASSET_NAME_PROPERTY)
+    private String name;
+
     @Column(name = SEARCH_TEXT_PROPERTY)
     private String searchText;
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
index 69ed92c..a7fe243 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/DeviceEntity.java
@@ -49,12 +49,13 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
     @Column(name = DEVICE_CUSTOMER_ID_PROPERTY)
     private UUID customerId;
 
-    @Column(name = DEVICE_NAME_PROPERTY)
-    private String name;
-
+    @PartitionKey(value = 3)
     @Column(name = DEVICE_TYPE_PROPERTY)
     private String type;
 
+    @Column(name = DEVICE_NAME_PROPERTY)
+    private String name;
+
     @Column(name = SEARCH_TEXT_PROPERTY)
     private String searchText;
     
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index 9f78e27..dfad10b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -124,8 +124,11 @@ public class ModelConstants {
     public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
 
     public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
+    public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";
     public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
+    public static final String DEVICE_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_by_type_and_search_text";
     public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
+    public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant";
 
     /**
      * Cassandra asset constants.
@@ -138,8 +141,30 @@ public class ModelConstants {
     public static final String ASSET_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
 
     public static final String ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_and_search_text";
+    public static final String ASSET_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_tenant_by_type_and_search_text";
     public static final String ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_customer_and_search_text";
+    public static final String ASSET_BY_CUSTOMER_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_customer_by_type_and_search_text";
     public static final String ASSET_BY_TENANT_AND_NAME_VIEW_NAME = "asset_by_tenant_and_name";
+    public static final String ASSET_TYPES_BY_TENANT_VIEW_NAME = "asset_types_by_tenant";
+
+    /**
+     * Cassandra alarm constants.
+     */
+    public static final String ALARM_COLUMN_FAMILY_NAME = "alarm";
+    public static final String ALARM_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
+    public static final String ALARM_TYPE_PROPERTY = "type";
+    public static final String ALARM_DETAILS_PROPERTY = "details";
+    public static final String ALARM_ORIGINATOR_ID_PROPERTY = "originator_id";
+    public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type";
+    public static final String ALARM_SEVERITY_PROPERTY = "severity";
+    public static final String ALARM_STATUS_PROPERTY = "status";
+    public static final String ALARM_START_TS_PROPERTY = "start_ts";
+    public static final String ALARM_END_TS_PROPERTY = "end_ts";
+    public static final String ALARM_ACK_TS_PROPERTY = "ack_ts";
+    public static final String ALARM_CLEAR_TS_PROPERTY = "clear_ts";
+    public static final String ALARM_PROPAGATE_PROPERTY = "propagate";
+
+    public static final String ALARM_BY_ID_VIEW_NAME = "alarm_by_id";
 
     /**
      * Cassandra entity relation constants.
@@ -150,7 +175,9 @@ public class ModelConstants {
     public static final String RELATION_TO_ID_PROPERTY = "to_id";
     public static final String RELATION_TO_TYPE_PROPERTY = "to_type";
     public static final String RELATION_TYPE_PROPERTY = "relation_type";
+    public static final String RELATION_TYPE_GROUP_PROPERTY = "relation_type_group";
 
+    public static final String RELATION_BY_TYPE_AND_CHILD_TYPE_VIEW_NAME = "relation_by_type_and_child_type";
     public static final String RELATION_REVERSE_VIEW_NAME = "reverse_relation";
 
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/TenantAssetTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/TenantAssetTypeEntity.java
new file mode 100644
index 0000000..36361ef
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/TenantAssetTypeEntity.java
@@ -0,0 +1,107 @@
+/**
+ * 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.dao.model;
+
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = ASSET_TYPES_BY_TENANT_VIEW_NAME)
+public class TenantAssetTypeEntity {
+
+    @Transient
+    private static final long serialVersionUID = -1268181161886910152L;
+
+    @PartitionKey(value = 0)
+    @Column(name = ASSET_TYPE_PROPERTY)
+    private String type;
+
+    @PartitionKey(value = 1)
+    @Column(name = ASSET_TENANT_ID_PROPERTY)
+    private UUID tenantId;
+
+    public TenantAssetTypeEntity() {
+        super();
+    }
+
+    public TenantAssetTypeEntity(TenantAssetType tenantAssetType) {
+        this.type = tenantAssetType.getType();
+        if (tenantAssetType.getTenantId() != null) {
+            this.tenantId = tenantAssetType.getTenantId().getId();
+        }
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public UUID getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(UUID tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantAssetTypeEntity that = (TenantAssetTypeEntity) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantAssetTypeEntity{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    public TenantAssetType toTenantAssetType() {
+        TenantAssetType tenantAssetType = new TenantAssetType();
+        tenantAssetType.setType(type);
+        if (tenantId != null) {
+            tenantAssetType.setTenantId(new TenantId(tenantId));
+        }
+        return tenantAssetType;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/TenantDeviceTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/TenantDeviceTypeEntity.java
new file mode 100644
index 0000000..dad954c
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/TenantDeviceTypeEntity.java
@@ -0,0 +1,107 @@
+/**
+ * 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.dao.model;
+
+import com.datastax.driver.mapping.annotations.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import org.thingsboard.server.common.data.TenantDeviceType;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = DEVICE_TYPES_BY_TENANT_VIEW_NAME)
+public class TenantDeviceTypeEntity {
+
+    @Transient
+    private static final long serialVersionUID = -1268181166886910152L;
+
+    @PartitionKey(value = 0)
+    @Column(name = DEVICE_TYPE_PROPERTY)
+    private String type;
+
+    @PartitionKey(value = 1)
+    @Column(name = DEVICE_TENANT_ID_PROPERTY)
+    private UUID tenantId;
+
+    public TenantDeviceTypeEntity() {
+        super();
+    }
+
+    public TenantDeviceTypeEntity(TenantDeviceType tenantDeviceType) {
+        this.type = tenantDeviceType.getType();
+        if (tenantDeviceType.getTenantId() != null) {
+            this.tenantId = tenantDeviceType.getTenantId().getId();
+        }
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public UUID getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(UUID tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = type != null ? type.hashCode() : 0;
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        TenantDeviceTypeEntity that = (TenantDeviceTypeEntity) o;
+
+        if (type != null ? !type.equals(that.type) : that.type != null) return false;
+        return tenantId != null ? tenantId.equals(that.tenantId) : that.tenantId == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("TenantDeviceTypeEntity{");
+        sb.append("type='").append(type).append('\'');
+        sb.append(", tenantId=").append(tenantId);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    public TenantDeviceType toTenantDeviceType() {
+        TenantDeviceType tenantDeviceType = new TenantDeviceType();
+        tenantDeviceType.setType(type);
+        if (tenantId != null) {
+            tenantDeviceType.setTenantId(new TenantId(tenantId));
+        }
+        return tenantDeviceType;
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.java
new file mode 100644
index 0000000..2f8e840
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmSeverityCodec.java
@@ -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.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.dao.alarm.AlarmService;
+
+public class AlarmSeverityCodec extends EnumNameCodec<AlarmSeverity> {
+
+    public AlarmSeverityCodec() {
+        super(AlarmSeverity.class);
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java
new file mode 100644
index 0000000..7ba7d7b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/AlarmStatusCodec.java
@@ -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.
+ */
+package org.thingsboard.server.dao.model.type;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+
+public class AlarmStatusCodec extends EnumNameCodec<AlarmStatus> {
+
+    public AlarmStatusCodec() {
+        super(AlarmStatus.class);
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java
new file mode 100644
index 0000000..1c0514e
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/RelationTypeGroupCodec.java
@@ -0,0 +1,28 @@
+/**
+ * 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.dao.model.type;
+
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+
+import com.datastax.driver.extras.codecs.enums.EnumNameCodec;
+
+public class RelationTypeGroupCodec extends EnumNameCodec<RelationTypeGroup> {
+
+    public RelationTypeGroupCodec() {
+        super(RelationTypeGroup.class);
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
index 5e0eb39..a0be00e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
@@ -22,7 +22,6 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -30,9 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
-import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.DatabaseException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -55,7 +53,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId;
 
 @Service
 @Slf4j
-public class BasePluginService extends BaseEntityService implements PluginService {
+public class BasePluginService extends AbstractEntityService implements PluginService {
 
     //TODO: move to a better place.
     public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
index 5fd6632..9fc2382 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -16,23 +16,34 @@
 package org.thingsboard.server.dao.relation;
 
 import com.datastax.driver.core.*;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
+import com.datastax.driver.core.querybuilder.Select;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.base.Function;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.AbstractAsyncDao;
+import org.thingsboard.server.dao.AbstractSearchTimeDao;
 import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.model.type.RelationTypeGroupCodec;
 
 import javax.annotation.Nullable;
 import javax.annotation.PostConstruct;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
+import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
+import static org.thingsboard.server.dao.model.ModelConstants.RELATION_COLUMN_FAMILY_NAME;
+
 /**
  * Created by ashvayka on 25.04.17.
  */
@@ -45,12 +56,15 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
             ModelConstants.RELATION_FROM_TYPE_PROPERTY + "," +
             ModelConstants.RELATION_TO_ID_PROPERTY + "," +
             ModelConstants.RELATION_TO_TYPE_PROPERTY + "," +
+            ModelConstants.RELATION_TYPE_GROUP_PROPERTY + "," +
             ModelConstants.RELATION_TYPE_PROPERTY + "," +
             ModelConstants.ADDITIONAL_INFO_PROPERTY;
     public static final String FROM = " FROM ";
     public static final String WHERE = " WHERE ";
     public static final String AND = " AND ";
 
+    private static final RelationTypeGroupCodec relationTypeGroupCodec = new RelationTypeGroupCodec();
+
     private PreparedStatement saveStmt;
     private PreparedStatement findAllByFromStmt;
     private PreparedStatement findAllByFromAndTypeStmt;
@@ -66,43 +80,52 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from) {
-        BoundStatement stmt = getFindAllByFromStmt().bind().setUUID(0, from.getId()).setString(1, from.getEntityType().name());
+    public ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from, RelationTypeGroup typeGroup) {
+        BoundStatement stmt = getFindAllByFromStmt().bind()
+                .setUUID(0, from.getId())
+                .setString(1, from.getEntityType().name())
+                .set(2, typeGroup, relationTypeGroupCodec);
         return executeAsyncRead(from, stmt);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType) {
+    public ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getFindAllByFromAndTypeStmt().bind()
                 .setUUID(0, from.getId())
                 .setString(1, from.getEntityType().name())
-                .setString(2, relationType);
+                .set(2, typeGroup, relationTypeGroupCodec)
+                .setString(3, relationType);
         return executeAsyncRead(from, stmt);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to) {
-        BoundStatement stmt = getFindAllByToStmt().bind().setUUID(0, to.getId()).setString(1, to.getEntityType().name());
+    public ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to, RelationTypeGroup typeGroup) {
+        BoundStatement stmt = getFindAllByToStmt().bind()
+                .setUUID(0, to.getId())
+                .setString(1, to.getEntityType().name())
+                .set(2, typeGroup, relationTypeGroupCodec);
         return executeAsyncRead(to, stmt);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType) {
+    public ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getFindAllByToAndTypeStmt().bind()
                 .setUUID(0, to.getId())
                 .setString(1, to.getEntityType().name())
-                .setString(2, relationType);
+                .set(2, typeGroup, relationTypeGroupCodec)
+                .setString(3, relationType);
         return executeAsyncRead(to, stmt);
     }
 
     @Override
-    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType) {
+    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getCheckRelationStmt().bind()
                 .setUUID(0, from.getId())
                 .setString(1, from.getEntityType().name())
                 .setUUID(2, to.getId())
                 .setString(3, to.getEntityType().name())
-                .setString(4, relationType);
+                .set(4, typeGroup, relationTypeGroupCodec)
+                .setString(5, relationType);
         return getFuture(executeAsyncRead(stmt), rs -> rs != null ? rs.one() != null : false);
     }
 
@@ -113,25 +136,27 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
                 .setString(1, relation.getFrom().getEntityType().name())
                 .setUUID(2, relation.getTo().getId())
                 .setString(3, relation.getTo().getEntityType().name())
-                .setString(4, relation.getType())
-                .set(5, relation.getAdditionalInfo(), JsonNode.class);
+                .set(4, relation.getTypeGroup(), relationTypeGroupCodec)
+                .setString(5, relation.getType())
+                .set(6, relation.getAdditionalInfo(), JsonNode.class);
         ResultSetFuture future = executeAsyncWrite(stmt);
         return getBooleanListenableFuture(future);
     }
 
     @Override
     public ListenableFuture<Boolean> deleteRelation(EntityRelation relation) {
-        return deleteRelation(relation.getFrom(), relation.getTo(), relation.getType());
+        return deleteRelation(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
     }
 
     @Override
-    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType) {
+    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
         BoundStatement stmt = getDeleteStmt().bind()
                 .setUUID(0, from.getId())
                 .setString(1, from.getEntityType().name())
                 .setUUID(2, to.getId())
                 .setString(3, to.getEntityType().name())
-                .setString(4, relationType);
+                .set(4, typeGroup, relationTypeGroupCodec)
+                .setString(5, relationType);
         ResultSetFuture future = executeAsyncWrite(stmt);
         return getBooleanListenableFuture(future);
     }
@@ -145,6 +170,21 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
         return getBooleanListenableFuture(future);
     }
 
+    @Override
+    public ListenableFuture<List<EntityRelation>> findRelations(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType childType, TimePageLink pageLink) {
+        Select.Where query = AbstractSearchTimeDao.buildQuery(ModelConstants.RELATION_BY_TYPE_AND_CHILD_TYPE_VIEW_NAME,
+                Arrays.asList(eq(ModelConstants.RELATION_FROM_ID_PROPERTY, from.getId()),
+                        eq(ModelConstants.RELATION_FROM_TYPE_PROPERTY, from.getEntityType().name()),
+                        eq(ModelConstants.RELATION_TYPE_GROUP_PROPERTY, typeGroup.name()),
+                        eq(ModelConstants.RELATION_TYPE_PROPERTY, relationType),
+                        eq(ModelConstants.RELATION_TO_TYPE_PROPERTY, childType.name())),
+                Arrays.asList(QueryBuilder.asc(ModelConstants.RELATION_TYPE_GROUP_PROPERTY),
+                        QueryBuilder.asc(ModelConstants.RELATION_TYPE_PROPERTY),
+                        QueryBuilder.asc(ModelConstants.RELATION_TO_TYPE_PROPERTY)),
+                pageLink, ModelConstants.RELATION_TO_ID_PROPERTY);
+        return getFuture(executeAsyncRead(query), rs -> getEntityRelations(rs));
+    }
+
     private PreparedStatement getSaveStmt() {
         if (saveStmt == null) {
             saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
@@ -152,9 +192,10 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
                     "," + ModelConstants.RELATION_FROM_TYPE_PROPERTY +
                     "," + ModelConstants.RELATION_TO_ID_PROPERTY +
                     "," + ModelConstants.RELATION_TO_TYPE_PROPERTY +
+                    "," + ModelConstants.RELATION_TYPE_GROUP_PROPERTY +
                     "," + ModelConstants.RELATION_TYPE_PROPERTY +
                     "," + ModelConstants.ADDITIONAL_INFO_PROPERTY + ")" +
-                    " VALUES(?, ?, ?, ?, ?, ?)");
+                    " VALUES(?, ?, ?, ?, ?, ?, ?)");
         }
         return saveStmt;
     }
@@ -166,6 +207,7 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ?" +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ?");
         }
         return deleteStmt;
@@ -185,7 +227,8 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
             findAllByFromStmt = getSession().prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " +
-                    AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? ");
+                    AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? ");
         }
         return findAllByFromStmt;
     }
@@ -196,17 +239,20 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
                     FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? ");
         }
         return findAllByFromAndTypeStmt;
     }
 
+
     private PreparedStatement getFindAllByToStmt() {
         if (findAllByToStmt == null) {
             findAllByToStmt = getSession().prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
                     WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " +
-                    AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? ");
+                    AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? ");
         }
         return findAllByToStmt;
     }
@@ -217,11 +263,13 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
                     FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
                     WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? ");
         }
         return findAllByToAndTypeStmt;
     }
 
+
     private PreparedStatement getCheckRelationStmt() {
         if (checkRelationStmt == null) {
             checkRelationStmt = getSession().prepare(SELECT_COLUMNS + " " +
@@ -230,36 +278,19 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TYPE_GROUP_PROPERTY + " = ? " +
                     AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ? ");
         }
         return checkRelationStmt;
     }
 
-    private EntityRelation getEntityRelation(Row row) {
-        EntityRelation relation = new EntityRelation();
-        relation.setType(row.getString(ModelConstants.RELATION_TYPE_PROPERTY));
-        relation.setAdditionalInfo(row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY, JsonNode.class));
-        relation.setFrom(toEntity(row, ModelConstants.RELATION_FROM_ID_PROPERTY, ModelConstants.RELATION_FROM_TYPE_PROPERTY));
-        relation.setTo(toEntity(row, ModelConstants.RELATION_TO_ID_PROPERTY, ModelConstants.RELATION_TO_TYPE_PROPERTY));
-        return relation;
-    }
-
     private EntityId toEntity(Row row, String uuidColumn, String typeColumn) {
         return EntityIdFactory.getByTypeAndUuid(row.getString(typeColumn), row.getUUID(uuidColumn));
     }
 
     private ListenableFuture<List<EntityRelation>> executeAsyncRead(EntityId from, BoundStatement stmt) {
         log.debug("Generated query [{}] for entity {}", stmt, from);
-        return getFuture(executeAsyncRead(stmt), rs -> {
-            List<Row> rows = rs.all();
-            List<EntityRelation> entries = new ArrayList<>(rows.size());
-            if (!rows.isEmpty()) {
-                rows.forEach(row -> {
-                    entries.add(getEntityRelation(row));
-                });
-            }
-            return entries;
-        });
+        return getFuture(executeAsyncRead(stmt), rs -> getEntityRelations(rs));
     }
 
     private ListenableFuture<Boolean> getBooleanListenableFuture(ResultSetFuture rsFuture) {
@@ -276,4 +307,25 @@ public class BaseRelationDao extends AbstractAsyncDao implements RelationDao {
         }, readResultsProcessingExecutor);
     }
 
+    private List<EntityRelation> getEntityRelations(ResultSet rs) {
+        List<Row> rows = rs.all();
+        List<EntityRelation> entries = new ArrayList<>(rows.size());
+        if (!rows.isEmpty()) {
+            rows.forEach(row -> {
+                entries.add(getEntityRelation(row));
+            });
+        }
+        return entries;
+    }
+
+    private EntityRelation getEntityRelation(Row row) {
+        EntityRelation relation = new EntityRelation();
+        relation.setTypeGroup(row.get(ModelConstants.RELATION_TYPE_GROUP_PROPERTY, relationTypeGroupCodec));
+        relation.setType(row.getString(ModelConstants.RELATION_TYPE_PROPERTY));
+        relation.setAdditionalInfo(row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY, JsonNode.class));
+        relation.setFrom(toEntity(row, ModelConstants.RELATION_FROM_ID_PROPERTY, ModelConstants.RELATION_FROM_TYPE_PROPERTY));
+        relation.setTo(toEntity(row, ModelConstants.RELATION_TO_ID_PROPERTY, ModelConstants.RELATION_TO_TYPE_PROPERTY));
+        return relation;
+    }
+
 }
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 1b572c6..36ec567 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
@@ -25,11 +25,15 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationInfo;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.entity.EntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 
 import javax.annotation.Nullable;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
 
 /**
  * Created by ashvayka on 28.04.17.
@@ -41,11 +45,14 @@ public class BaseRelationService implements RelationService {
     @Autowired
     private RelationDao relationDao;
 
+    @Autowired
+    private EntityService entityService;
+
     @Override
-    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType) {
-        log.trace("Executing checkRelation [{}][{}][{}]", from, to, relationType);
-        validate(from, to, relationType);
-        return relationDao.checkRelation(from, to, relationType);
+    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        log.trace("Executing checkRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
+        validate(from, to, relationType, typeGroup);
+        return relationDao.checkRelation(from, to, relationType, typeGroup);
     }
 
     @Override
@@ -63,23 +70,28 @@ public class BaseRelationService implements RelationService {
     }
 
     @Override
-    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType) {
-        log.trace("Executing deleteRelation [{}][{}][{}]", from, to, relationType);
-        validate(from, to, relationType);
-        return relationDao.deleteRelation(from, to, relationType);
+    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup);
+        validate(from, to, relationType, typeGroup);
+        return relationDao.deleteRelation(from, to, relationType, typeGroup);
     }
 
     @Override
     public ListenableFuture<Boolean> deleteEntityRelations(EntityId entity) {
         log.trace("Executing deleteEntityRelations [{}]", entity);
         validate(entity);
-        ListenableFuture<List<EntityRelation>> inboundRelations = relationDao.findAllByTo(entity);
-        ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction<List<EntityRelation>, List<Boolean>>() {
+        List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
+        for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
+            inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
+        }
+        Futures.allAsList(inboundRelationsList);
+        ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
+        ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction<List<List<EntityRelation>>, List<Boolean>>() {
             @Override
-            public ListenableFuture<List<Boolean>> apply(List<EntityRelation> relations) throws Exception {
+            public ListenableFuture<List<Boolean>> apply(List<List<EntityRelation>> relations) throws Exception {
                 List<ListenableFuture<Boolean>> results = new ArrayList<>();
-                for (EntityRelation relation : relations) {
-                    results.add(relationDao.deleteRelation(relation));
+                for (List<EntityRelation> relationList : relations) {
+                    relationList.stream().forEach(relation -> results.add(relationDao.deleteRelation(relation)));
                 }
                 return Futures.allAsList(results);
             }
@@ -93,33 +105,88 @@ public class BaseRelationService implements RelationService {
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByFrom(EntityId from) {
-        log.trace("Executing findByFrom [{}]", from);
+    public ListenableFuture<List<EntityRelation>> findByFrom(EntityId from, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByFrom [{}][{}]", from, typeGroup);
         validate(from);
-        return relationDao.findAllByFrom(from);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByFrom(from, typeGroup);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType) {
-        log.trace("Executing findByFromAndType [{}][{}]", from, relationType);
+    public ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from, RelationTypeGroup typeGroup) {
+        log.trace("Executing findInfoByFrom [{}][{}]", from, typeGroup);
+        validate(from);
+        validateTypeGroup(typeGroup);
+        ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByFrom(from, typeGroup);
+        ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
+                (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+            List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
+                    relations1.stream().forEach(relation ->
+                            futures.add(fetchRelationInfoAsync(relation,
+                                    relation2 -> relation2.getTo(),
+                                    (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName)))
+                    );
+                    return Futures.successfulAsList(futures);
+        });
+        return relationsInfo;
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup);
         validate(from);
         validateType(relationType);
-        return relationDao.findAllByFromAndType(from, relationType);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByFromAndType(from, relationType, typeGroup);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByTo(EntityId to) {
-        log.trace("Executing findByTo [{}]", to);
+    public ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByTo [{}][{}]", to, typeGroup);
         validate(to);
-        return relationDao.findAllByTo(to);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByTo(to, typeGroup);
     }
 
     @Override
-    public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType) {
-        log.trace("Executing findByToAndType [{}][{}]", to, relationType);
+    public ListenableFuture<List<EntityRelationInfo>> findInfoByTo(EntityId to, RelationTypeGroup typeGroup) {
+        log.trace("Executing findInfoByTo [{}][{}]", to, typeGroup);
+        validate(to);
+        validateTypeGroup(typeGroup);
+        ListenableFuture<List<EntityRelation>> relations = relationDao.findAllByTo(to, typeGroup);
+        ListenableFuture<List<EntityRelationInfo>> relationsInfo = Futures.transform(relations,
+                (AsyncFunction<List<EntityRelation>, List<EntityRelationInfo>>) relations1 -> {
+                    List<ListenableFuture<EntityRelationInfo>> futures = new ArrayList<>();
+                    relations1.stream().forEach(relation ->
+                        futures.add(fetchRelationInfoAsync(relation,
+                                relation2 -> relation2.getFrom(),
+                                (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName)))
+                    );
+                    return Futures.successfulAsList(futures);
+                });
+        return relationsInfo;
+    }
+
+    private ListenableFuture<EntityRelationInfo> fetchRelationInfoAsync(EntityRelation relation,
+                                                                        Function<EntityRelation, EntityId> entityIdGetter,
+                                                                        BiConsumer<EntityRelationInfo, String> entityNameSetter) {
+        ListenableFuture<String> entityName = entityService.fetchEntityNameAsync(entityIdGetter.apply(relation));
+        ListenableFuture<EntityRelationInfo> entityRelationInfo =
+                Futures.transform(entityName, (Function<String, EntityRelationInfo>) entityName1 -> {
+                    EntityRelationInfo entityRelationInfo1 = new EntityRelationInfo(relation);
+                    entityNameSetter.accept(entityRelationInfo1, entityName1);
+                    return entityRelationInfo1;
+                });
+        return entityRelationInfo;
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) {
+        log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup);
         validate(to);
         validateType(relationType);
-        return relationDao.findAllByToAndType(to, relationType);
+        validateTypeGroup(typeGroup);
+        return relationDao.findAllByToAndType(to, relationType, typeGroup);
     }
 
     @Override
@@ -128,8 +195,7 @@ public class BaseRelationService implements RelationService {
         RelationsSearchParameters params = query.getParameters();
         final List<EntityTypeFilter> filters = query.getFilters();
         if (filters == null || filters.isEmpty()) {
-            log.warn("Failed to query relations. Filters are not set [{}]", query);
-            throw new RuntimeException("Filters are not set!");
+            log.debug("Filters are not set [{}]", query);
         }
 
         int maxLvl = params.getMaxLevel() > 0 ? params.getMaxLevel() : Integer.MAX_VALUE;
@@ -139,10 +205,14 @@ public class BaseRelationService implements RelationService {
             return Futures.transform(relationSet, (Function<Set<EntityRelation>, List<EntityRelation>>) input -> {
                 List<EntityRelation> relations = new ArrayList<>();
                 for (EntityRelation relation : input) {
-                    for (EntityTypeFilter filter : filters) {
-                        if (match(filter, relation, params.getDirection())) {
-                            relations.add(relation);
-                            break;
+                    if (filters == null || filters.isEmpty()) {
+                        relations.add(relation);
+                    } else {
+                        for (EntityTypeFilter filter : filters) {
+                            if (match(filter, relation, params.getDirection())) {
+                                relations.add(relation);
+                                break;
+                            }
                         }
                     }
                 }
@@ -158,11 +228,12 @@ public class BaseRelationService implements RelationService {
         if (relation == null) {
             throw new DataValidationException("Relation type should be specified!");
         }
-        validate(relation.getFrom(), relation.getTo(), relation.getType());
+        validate(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
     }
 
-    protected void validate(EntityId from, EntityId to, String type) {
+    protected void validate(EntityId from, EntityId to, String type, RelationTypeGroup typeGroup) {
         validateType(type);
+        validateTypeGroup(typeGroup);
         if (from == null) {
             throw new DataValidationException("Relation should contain from entity!");
         }
@@ -177,6 +248,12 @@ public class BaseRelationService implements RelationService {
         }
     }
 
+    private void validateTypeGroup(RelationTypeGroup typeGroup) {
+        if (typeGroup == null) {
+            throw new DataValidationException("Relation type group should be specified!");
+        }
+    }
+
     protected void validate(EntityId entity) {
         if (entity == null) {
             throw new DataValidationException("Entity should be specified!");
@@ -211,7 +288,8 @@ public class BaseRelationService implements RelationService {
         }
     }
 
-    private ListenableFuture<Set<EntityRelation>> findRelationsRecursively(final EntityId rootId, final EntitySearchDirection direction, int lvl, final ConcurrentHashMap<EntityId, Boolean> uniqueMap) throws Exception {
+    private ListenableFuture<Set<EntityRelation>> findRelationsRecursively(final EntityId rootId, final EntitySearchDirection direction, int lvl,
+                                                                           final ConcurrentHashMap<EntityId, Boolean> uniqueMap) throws Exception {
         if (lvl == 0) {
             return Futures.immediateFuture(Collections.emptySet());
         }
@@ -247,9 +325,9 @@ public class BaseRelationService implements RelationService {
     private ListenableFuture<List<EntityRelation>> findRelations(final EntityId rootId, final EntitySearchDirection direction) {
         ListenableFuture<List<EntityRelation>> relations;
         if (direction == EntitySearchDirection.FROM) {
-            relations = findByFrom(rootId);
+            relations = findByFrom(rootId, RelationTypeGroup.COMMON);
         } else {
-            relations = findByTo(rootId);
+            relations = findByTo(rootId, RelationTypeGroup.COMMON);
         }
         return relations;
     }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
index df47259..3abadb8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
@@ -16,8 +16,11 @@
 package org.thingsboard.server.dao.relation;
 
 import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 
 import java.util.List;
 
@@ -26,22 +29,24 @@ import java.util.List;
  */
 public interface RelationDao {
 
-    ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from);
+    ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType);
+    ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to);
+    ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType);
+    ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup);
 
-    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> saveRelation(EntityRelation relation);
 
     ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
 
-    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> deleteOutboundRelations(EntityId entity);
 
+    ListenableFuture<List<EntityRelation>> findRelations(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType toType, TimePageLink pageLink);
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
index f4f3a37..a810454 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -18,6 +18,8 @@ package org.thingsboard.server.dao.relation;
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationInfo;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 
 import java.util.List;
 
@@ -26,24 +28,31 @@ import java.util.List;
  */
 public interface RelationService {
 
-    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> saveRelation(EntityRelation relation);
 
     ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
 
-    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<Boolean> deleteEntityRelations(EntityId entity);
 
-    ListenableFuture<List<EntityRelation>> findByFrom(EntityId from);
+    ListenableFuture<List<EntityRelation>> findByFrom(EntityId from, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType);
+    ListenableFuture<List<EntityRelationInfo>> findInfoByFrom(EntityId from, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findByTo(EntityId to);
+    ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup);
 
-    ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType);
+    ListenableFuture<List<EntityRelation>> findByTo(EntityId to, RelationTypeGroup typeGroup);
+
+    ListenableFuture<List<EntityRelationInfo>> findInfoByTo(EntityId to, RelationTypeGroup typeGroup);
+
+    ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup);
 
     ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query);
 
+//    TODO: This method may be useful for some validations in the future
+//    ListenableFuture<Boolean> checkRecursiveRelation(EntityId from, EntityId to);
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
index fb5e9ce..350fc3a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
@@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -34,11 +33,10 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.DatabaseException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.AssetEntity;
 import org.thingsboard.server.dao.model.RuleMetaDataEntity;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -58,7 +56,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
 
 @Service
 @Slf4j
-public class BaseRuleService extends BaseEntityService implements RuleService {
+public class BaseRuleService extends AbstractEntityService implements RuleService {
 
     private final TenantId systemTenantId = new TenantId(NULL_UUID);
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
index bcc31a8..e6f5011 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
@@ -26,7 +26,6 @@ import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
-import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -34,9 +33,8 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceService;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
-import org.thingsboard.server.dao.model.CustomerEntity;
 import org.thingsboard.server.dao.model.TenantEntity;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.rule.RuleService;
@@ -50,7 +48,7 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
 
 @Service
 @Slf4j
-public class TenantServiceImpl extends BaseEntityService implements TenantService {
+public class TenantServiceImpl extends AbstractEntityService implements TenantService {
 
     private static final String DEFAULT_TENANT_REGION = "Global";
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
index f4043a0..47e800c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.user;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -27,6 +28,8 @@ public interface UserService {
 	
 	public User findUserById(UserId userId);
 
+	public ListenableFuture<User> findUserByIdAsync(UserId userId);
+
 	public User findUserByEmail(String email);
 	
 	public User saveUser(User user);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
index 6410111..125040b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
@@ -23,6 +23,9 @@ import static org.thingsboard.server.dao.service.Validator.validateString;
 
 import java.util.List;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -35,7 +38,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
 import org.thingsboard.server.dao.customer.CustomerDao;
-import org.thingsboard.server.dao.entity.BaseEntityService;
+import org.thingsboard.server.dao.entity.AbstractEntityService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.*;
@@ -47,7 +50,7 @@ import org.springframework.stereotype.Service;
 
 @Service
 @Slf4j
-public class UserServiceImpl extends BaseEntityService implements UserService {
+public class UserServiceImpl extends AbstractEntityService implements UserService {
 
     @Autowired
     private UserDao userDao;
@@ -78,6 +81,14 @@ public class UserServiceImpl extends BaseEntityService implements UserService {
 	}
 
     @Override
+    public ListenableFuture<User> findUserByIdAsync(UserId userId) {
+        log.trace("Executing findUserByIdAsync [{}]", userId);
+        validateId(userId, "Incorrect userId " + userId);
+        ListenableFuture<UserEntity> userEntity = userDao.findByIdAsync(userId.getId());
+        return Futures.transform(userEntity, (Function<? super UserEntity, ? extends User>) input -> getData(input));
+    }
+
+    @Override
     public User saveUser(User user) {
         log.trace("Executing saveUser [{}]", user);
         userValidator.validate(user);
diff --git a/dao/src/main/resources/demo-data.cql b/dao/src/main/resources/demo-data.cql
index 023cf1b..47d8de4 100644
--- a/dao/src/main/resources/demo-data.cql
+++ b/dao/src/main/resources/demo-data.cql
@@ -149,66 +149,73 @@ VALUES (
 
 /** Demo device **/
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+	'default',
 	'Test Device A1',
 	'test device a1'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0001' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+	'default',
 	'Test Device A2',
 	'test device a2'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0002' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0000' ),
+	'default',
 	'Test Device A3',
 	'test device a3'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0003' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0001' ),
+	'default',
 	'Test Device B1',
 	'test device b1'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text)
 VALUES (
 	minTimeuuid ( '2016-11-01 01:02:05+0004' ),
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( '2016-11-01 01:02:03+0002' ),
+	'default',
 	'Test Device C1',
 	'test device c1'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text, additional_info)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text, additional_info)
 VALUES (
 	c8f1a6f0-b993-11e6-8a04-9ff4e1b7933c,
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( 0 ),
+	'default',
 	'DHT11 Demo Device',
 	'dht11 demo device',
 	'{"description":"Demo device that is used in sample applications that upload data from DHT11 temperature and humidity sensor"}'
 );
 
-INSERT INTO thingsboard.device ( id, tenant_id, customer_id, name, search_text, additional_info)
+INSERT INTO thingsboard.device ( id, tenant_id, customer_id, type, name, search_text, additional_info)
 VALUES (
 	c8f1a6f0-b993-11e6-8a04-9ff4e1b7933d,
 	minTimeuuid ( '2016-11-01 01:02:01+0000' ),
 	minTimeuuid ( 0 ),
+	'default',
 	'Raspberry Pi Demo Device',
 	'raspberry pi demo device',
 	'{"description":"Demo device that is used in Raspberry Pi GPIO control sample application"}'
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
index c3e7cda..71de677 100644
--- a/dao/src/main/resources/schema.cql
+++ b/dao/src/main/resources/schema.cql
@@ -152,36 +152,57 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search
 	WITH CLUSTERING ORDER BY ( search_text ASC, id DESC );
 
 CREATE TABLE IF NOT EXISTS thingsboard.device (
-	id timeuuid,
-	tenant_id timeuuid,
-	customer_id timeuuid,
-	name text,
-	type text,
-	search_text text,
-	additional_info text,
-	PRIMARY KEY (id, tenant_id, customer_id)
+    id timeuuid,
+    tenant_id timeuuid,
+    customer_id timeuuid,
+    name text,
+    type text,
+    search_text text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id, customer_id, type)
 );
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_name AS
-	SELECT *
-	from thingsboard.device
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, name, id, customer_id)
-	WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, name, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_and_search_text AS
-	SELECT *
-	from thingsboard.device
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, search_text, id, customer_id)
-	WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_tenant_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, type, search_text, id, customer_id)
+    WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_and_search_text AS
-	SELECT *
-	from thingsboard.device
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( customer_id, tenant_id, search_text, id )
-	WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, search_text, id, type )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_by_customer_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, type, search_text, id )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_types_by_tenant AS
+    SELECT *
+    from thingsboard.device
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( (type, tenant_id), id, customer_id)
+    WITH CLUSTERING ORDER BY ( id ASC, customer_id DESC);
 
 CREATE TABLE IF NOT EXISTS thingsboard.device_credentials (
 	id timeuuid PRIMARY KEY,
@@ -203,55 +224,108 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.device_credentials_by_credent
 	WHERE credentials_id IS NOT NULL AND id IS NOT NULL
 	PRIMARY KEY ( credentials_id, id );
 
-
 CREATE TABLE IF NOT EXISTS thingsboard.asset (
-	id timeuuid,
-	tenant_id timeuuid,
-	customer_id timeuuid,
-	name text,
-	type text,
-	search_text text,
-	additional_info text,
-	PRIMARY KEY (id, tenant_id, customer_id)
+    id timeuuid,
+    tenant_id timeuuid,
+    customer_id timeuuid,
+    name text,
+    type text,
+    search_text text,
+    additional_info text,
+    PRIMARY KEY (id, tenant_id, customer_id, type)
 );
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_name AS
-	SELECT *
-	from thingsboard.asset
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, name, id, customer_id)
-	WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND name IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, name, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( name ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_and_search_text AS
-	SELECT *
-	from thingsboard.asset
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( tenant_id, search_text, id, customer_id)
-	WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, search_text, id, customer_id, type)
+    WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_tenant_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( tenant_id, type, search_text, id, customer_id)
+    WITH CLUSTERING ORDER BY ( type ASC, search_text ASC, id DESC, customer_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_and_search_text AS
-	SELECT *
-	from thingsboard.asset
-	WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
-	PRIMARY KEY ( customer_id, tenant_id, search_text, id )
-	WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, search_text, id, type )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_by_customer_by_type_and_search_text AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( customer_id, tenant_id, type, search_text, id )
+    WITH CLUSTERING ORDER BY ( tenant_id DESC, type ASC, search_text ASC, id DESC );
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.asset_types_by_tenant AS
+    SELECT *
+    from thingsboard.asset
+    WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY ( (type, tenant_id), id, customer_id)
+    WITH CLUSTERING ORDER BY ( id ASC, customer_id DESC);
+
+CREATE TABLE IF NOT EXISTS thingsboard.alarm (
+	id timeuuid,
+	tenant_id timeuuid,
+	type text,
+	originator_id timeuuid,
+	originator_type text,
+    severity text,
+    status text,
+	start_ts bigint,
+	end_ts bigint,
+	ack_ts bigint,
+	clear_ts bigint,
+	details text,
+	propagate boolean,
+	PRIMARY KEY ((tenant_id, originator_id, originator_type), type, id)
+) WITH CLUSTERING ORDER BY ( type ASC, id DESC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.alarm_by_id AS
+    SELECT *
+    from thingsboard.alarm
+    WHERE tenant_id IS NOT NULL AND originator_id IS NOT NULL AND originator_type IS NOT NULL AND type IS NOT NULL
+    AND type IS NOT NULL AND id IS NOT NULL
+    PRIMARY KEY (id, tenant_id, originator_id, originator_type, type)
+    WITH CLUSTERING ORDER BY ( tenant_id ASC, originator_id ASC, originator_type ASC, type ASC);
 
 CREATE TABLE IF NOT EXISTS thingsboard.relation (
 	from_id timeuuid,
 	from_type text,
 	to_id timeuuid,
 	to_type text,
+	relation_type_group text,
 	relation_type text,
 	additional_info text,
-	PRIMARY KEY ((from_id, from_type), relation_type, to_id, to_type)
-);
+	PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_id, to_type)
+) WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_id ASC, to_type ASC);
+
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.relation_by_type_and_child_type AS
+    SELECT *
+    from thingsboard.relation
+    WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
+    PRIMARY KEY ((from_id, from_type), relation_type_group, relation_type, to_type, to_id)
+    WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, to_type ASC, to_id DESC);
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.reverse_relation AS
-SELECT *
-from thingsboard.relation
-WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
-PRIMARY KEY ((to_id, to_type), relation_type, from_id, from_type)
-WITH CLUSTERING ORDER BY ( relation_type ASC, from_id ASC, from_type ASC);
+    SELECT *
+    from thingsboard.relation
+    WHERE from_id IS NOT NULL AND from_type IS NOT NULL AND relation_type_group IS NOT NULL AND relation_type IS NOT NULL AND to_id IS NOT NULL AND to_type IS NOT NULL
+    PRIMARY KEY ((to_id, to_type), relation_type_group, relation_type, from_id, from_type)
+    WITH CLUSTERING ORDER BY ( relation_type_group ASC, relation_type ASC, from_id ASC, from_type ASC);
 
 CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle (
     id timeuuid,
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index 903207c..95340e6 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -40,6 +40,8 @@ import org.thingsboard.server.common.data.plugin.ComponentScope;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -88,6 +90,9 @@ public abstract class AbstractServiceTest {
     protected DeviceService deviceService;
 
     @Autowired
+    protected AssetService assetService;
+
+    @Autowired
     protected DeviceCredentialsService deviceCredentialsService;
 
     @Autowired
@@ -115,6 +120,9 @@ public abstract class AbstractServiceTest {
     protected RelationService relationService;
 
     @Autowired
+    protected AlarmService alarmService;
+
+    @Autowired
     private ComponentDescriptorService componentDescriptorService;
 
     class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
new file mode 100644
index 0000000..3b72574
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
@@ -0,0 +1,172 @@
+/**
+ * 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.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmQuery;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
+import org.thingsboard.server.dao.relation.EntityTypeFilter;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class AlarmServiceTest extends AbstractServiceTest {
+
+    public static final String TEST_ALARM = "TEST_ALARM";
+    private TenantId tenantId;
+
+    @Before
+    public void before() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = tenantService.saveTenant(tenant);
+        Assert.assertNotNull(savedTenant);
+        tenantId = savedTenant.getId();
+    }
+
+    @After
+    public void after() {
+        tenantService.deleteTenant(tenantId);
+    }
+
+
+    @Test
+    public void testSaveAndFetchAlarm() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+
+        Assert.assertTrue(relationService.saveRelation(relation).get());
+
+        long ts = System.currentTimeMillis();
+        Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId)
+                .type(TEST_ALARM)
+                .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK)
+                .startTs(ts).build();
+
+        Alarm created = alarmService.createOrUpdateAlarm(alarm);
+
+        Assert.assertNotNull(created);
+        Assert.assertNotNull(created.getId());
+        Assert.assertNotNull(created.getOriginator());
+        Assert.assertNotNull(created.getSeverity());
+        Assert.assertNotNull(created.getStatus());
+
+        Assert.assertEquals(tenantId, created.getTenantId());
+        Assert.assertEquals(childId, created.getOriginator());
+        Assert.assertEquals(TEST_ALARM, created.getType());
+        Assert.assertEquals(AlarmSeverity.CRITICAL, created.getSeverity());
+        Assert.assertEquals(AlarmStatus.ACTIVE_UNACK, created.getStatus());
+        Assert.assertEquals(ts, created.getStartTs());
+        Assert.assertEquals(ts, created.getEndTs());
+        Assert.assertEquals(0L, created.getAckTs());
+        Assert.assertEquals(0L, created.getClearTs());
+
+        Alarm fetched = alarmService.findAlarmByIdAsync(created.getId()).get();
+        Assert.assertEquals(created, fetched);
+    }
+
+    @Test
+    public void testFindAlarm() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+
+        Assert.assertTrue(relationService.saveRelation(relation).get());
+
+        long ts = System.currentTimeMillis();
+        Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId)
+                .type(TEST_ALARM)
+                .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK)
+                .startTs(ts).build();
+
+        Alarm created = alarmService.createOrUpdateAlarm(alarm);
+
+        // Check child relation
+        TimePageData<Alarm> alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        // Check parent relation
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(parentId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        alarmService.ackAlarm(created.getId(), System.currentTimeMillis()).get();
+        created = alarmService.findAlarmByIdAsync(created.getId()).get();
+
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_ACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+
+        // Check not existing relation
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.ACTIVE_UNACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(0, alarms.getData().size());
+
+        alarmService.clearAlarm(created.getId(), System.currentTimeMillis()).get();
+        created = alarmService.findAlarmByIdAsync(created.getId()).get();
+
+        alarms = alarmService.findAlarms(AlarmQuery.builder()
+                .affectedEntityId(childId)
+                .status(AlarmStatus.CLEARED_ACK).pageLink(
+                        new TimePageLink(1, 0L, System.currentTimeMillis(), false)
+                ).build()).get();
+        Assert.assertNotNull(alarms.getData());
+        Assert.assertEquals(1, alarms.getData().size());
+        Assert.assertEquals(created, alarms.getData().get(0));
+    }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java
new file mode 100644
index 0000000..9587703
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java
@@ -0,0 +1,634 @@
+/**
+ * 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.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.asset.TenantAssetType;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.exception.DataValidationException;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+
+public class BaseAssetServiceTest extends AbstractServiceTest {
+
+    private IdComparator<Asset> idComparator = new IdComparator<>();
+
+    private TenantId tenantId;
+
+    @Before
+    public void before() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = tenantService.saveTenant(tenant);
+        Assert.assertNotNull(savedTenant);
+        tenantId = savedTenant.getId();
+    }
+
+    @After
+    public void after() {
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testSaveAsset() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = assetService.saveAsset(asset);
+
+        Assert.assertNotNull(savedAsset);
+        Assert.assertNotNull(savedAsset.getId());
+        Assert.assertTrue(savedAsset.getCreatedTime() > 0);
+        Assert.assertEquals(asset.getTenantId(), savedAsset.getTenantId());
+        Assert.assertNotNull(savedAsset.getCustomerId());
+        Assert.assertEquals(NULL_UUID, savedAsset.getCustomerId().getId());
+        Assert.assertEquals(asset.getName(), savedAsset.getName());
+
+        savedAsset.setName("My new asset");
+
+        assetService.saveAsset(savedAsset);
+        Asset foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertEquals(foundAsset.getName(), savedAsset.getName());
+
+        assetService.deleteAsset(savedAsset.getId());
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveAssetWithEmptyName() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setType("default");
+        assetService.saveAsset(asset);
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveAssetWithEmptyTenant() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        assetService.saveAsset(asset);
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveAssetWithInvalidTenant() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        asset.setTenantId(new TenantId(UUIDs.timeBased()));
+        assetService.saveAsset(asset);
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testAssignAssetToNonExistentCustomer() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        asset.setTenantId(tenantId);
+        asset = assetService.saveAsset(asset);
+        try {
+            assetService.assignAssetToCustomer(asset.getId(), new CustomerId(UUIDs.timeBased()));
+        } finally {
+            assetService.deleteAsset(asset.getId());
+        }
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testAssignAssetToCustomerFromDifferentTenant() {
+        Asset asset = new Asset();
+        asset.setName("My asset");
+        asset.setType("default");
+        asset.setTenantId(tenantId);
+        asset = assetService.saveAsset(asset);
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Test different tenant");
+        tenant = tenantService.saveTenant(tenant);
+        Customer customer = new Customer();
+        customer.setTenantId(tenant.getId());
+        customer.setTitle("Test different customer");
+        customer = customerService.saveCustomer(customer);
+        try {
+            assetService.assignAssetToCustomer(asset.getId(), customer.getId());
+        } finally {
+            assetService.deleteAsset(asset.getId());
+            tenantService.deleteTenant(tenant.getId());
+        }
+    }
+
+    @Test
+    public void testFindAssetById() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = assetService.saveAsset(asset);
+        Asset foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertNotNull(foundAsset);
+        Assert.assertEquals(savedAsset, foundAsset);
+        assetService.deleteAsset(savedAsset.getId());
+    }
+
+    @Test
+    public void testFindAssetTypesByTenantId() throws Exception {
+        List<Asset> assets = new ArrayList<>();
+        try {
+            for (int i=0;i<3;i++) {
+                Asset asset = new Asset();
+                asset.setTenantId(tenantId);
+                asset.setName("My asset B"+i);
+                asset.setType("typeB");
+                assets.add(assetService.saveAsset(asset));
+            }
+            for (int i=0;i<7;i++) {
+                Asset asset = new Asset();
+                asset.setTenantId(tenantId);
+                asset.setName("My asset C"+i);
+                asset.setType("typeC");
+                assets.add(assetService.saveAsset(asset));
+            }
+            for (int i=0;i<9;i++) {
+                Asset asset = new Asset();
+                asset.setTenantId(tenantId);
+                asset.setName("My asset A"+i);
+                asset.setType("typeA");
+                assets.add(assetService.saveAsset(asset));
+            }
+            List<TenantAssetType> assetTypes = assetService.findAssetTypesByTenantId(tenantId).get();
+            Assert.assertNotNull(assetTypes);
+            Assert.assertEquals(3, assetTypes.size());
+            Assert.assertEquals("typeA", assetTypes.get(0).getType());
+            Assert.assertEquals("typeB", assetTypes.get(1).getType());
+            Assert.assertEquals("typeC", assetTypes.get(2).getType());
+        } finally {
+            assets.forEach((asset) -> { assetService.deleteAsset(asset.getId()); });
+        }
+    }
+
+    @Test
+    public void testDeleteAsset() {
+        Asset asset = new Asset();
+        asset.setTenantId(tenantId);
+        asset.setName("My asset");
+        asset.setType("default");
+        Asset savedAsset = assetService.saveAsset(asset);
+        Asset foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertNotNull(foundAsset);
+        assetService.deleteAsset(savedAsset.getId());
+        foundAsset = assetService.findAssetById(savedAsset.getId());
+        Assert.assertNull(foundAsset);
+    }
+
+    @Test
+    public void testFindAssetsByTenantId() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Test tenant");
+        tenant = tenantService.saveTenant(tenant);
+
+        TenantId tenantId = tenant.getId();
+
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<178;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            assets.add(assetService.saveAsset(asset));
+        }
+
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+
+        assetService.deleteAssetsByTenantId(tenantId);
+
+        pageLink = new TextPageLink(33);
+        pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertTrue(pageData.getData().isEmpty());
+
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdAndName() {
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle1.add(assetService.saveAsset(asset));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            assetsTitle2.add(assetService.saveAsset(asset));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = assetService.findAssetsByTenantId(tenantId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdAndType() {
+        String title1 = "Asset title 1";
+        String type1 = "typeA";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            assetsType1.add(assetService.saveAsset(asset));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeB";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            assetsType2.add(assetService.saveAsset(asset));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndType(tenantId, type1, pageLink);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = assetService.findAssetsByTenantIdAndType(tenantId, type2, pageLink);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndType(tenantId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndType(tenantId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdAndCustomerId() {
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Test tenant");
+        tenant = tenantService.saveTenant(tenant);
+
+        TenantId tenantId = tenant.getId();
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        List<Asset> assets = new ArrayList<>();
+        for (int i=0;i<278;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            asset.setName("Asset"+i);
+            asset.setType("default");
+            asset = assetService.saveAsset(asset);
+            assets.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+
+        List<Asset> loadedAssets = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+            loadedAssets.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assets, idComparator);
+        Collections.sort(loadedAssets, idComparator);
+
+        Assert.assertEquals(assets, loadedAssets);
+
+        assetService.unassignCustomerAssets(tenantId, customerId);
+
+        pageLink = new TextPageLink(33);
+        pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertTrue(pageData.getData().isEmpty());
+
+        tenantService.deleteTenant(tenantId);
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdCustomerIdAndName() {
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        List<Asset> assetsTitle1 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = assetService.saveAsset(asset);
+            assetsTitle1.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+        String title2 = "Asset title 2";
+        List<Asset> assetsTitle2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType("default");
+            asset = assetService.saveAsset(asset);
+            assetsTitle2.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+
+        List<Asset> loadedAssetsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+            loadedAssetsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle1, idComparator);
+        Collections.sort(loadedAssetsTitle1, idComparator);
+
+        Assert.assertEquals(assetsTitle1, loadedAssetsTitle1);
+
+        List<Asset> loadedAssetsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+            loadedAssetsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsTitle2, idComparator);
+        Collections.sort(loadedAssetsTitle2, idComparator);
+
+        Assert.assertEquals(assetsTitle2, loadedAssetsTitle2);
+
+        for (Asset asset : loadedAssetsTitle1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title1);
+        pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsTitle2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4, title2);
+        pageData = assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        customerService.deleteCustomer(customerId);
+    }
+
+    @Test
+    public void testFindAssetsByTenantIdCustomerIdAndType() {
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Asset title 1";
+        String type1 = "typeC";
+        List<Asset> assetsType1 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type1);
+            asset = assetService.saveAsset(asset);
+            assetsType1.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+        String title2 = "Asset title 2";
+        String type2 = "typeD";
+        List<Asset> assetsType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Asset asset = new Asset();
+            asset.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            asset.setName(name);
+            asset.setType(type2);
+            asset = assetService.saveAsset(asset);
+            assetsType2.add(assetService.assignAssetToCustomer(asset.getId(), customerId));
+        }
+
+        List<Asset> loadedAssetsType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Asset> pageData = null;
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+            loadedAssetsType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType1, idComparator);
+        Collections.sort(loadedAssetsType1, idComparator);
+
+        Assert.assertEquals(assetsType1, loadedAssetsType1);
+
+        List<Asset> loadedAssetsType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+            loadedAssetsType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(assetsType2, idComparator);
+        Collections.sort(loadedAssetsType2, idComparator);
+
+        Assert.assertEquals(assetsType2, loadedAssetsType2);
+
+        for (Asset asset : loadedAssetsType1) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Asset asset : loadedAssetsType2) {
+            assetService.deleteAsset(asset.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = assetService.findAssetsByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        customerService.deleteCustomer(customerId);
+    }
+
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
index efdab8a..4d9ef9f 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceImplTest.java
@@ -58,6 +58,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithEmptyDevice() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -73,6 +74,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsType() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -88,6 +90,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithEmptyCredentialsId() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -103,6 +106,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveNonExistentDeviceCredentials() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -122,6 +126,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithNonExistentDevice() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -137,6 +142,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceCredentialsWithInvalidCredemtialsIdLength() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getId());
@@ -153,6 +159,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
         Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
@@ -166,6 +173,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
         Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
@@ -181,6 +189,7 @@ public class DeviceCredentialsServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedDevice.getId());
         Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
index e9113d4..4f0bc8e 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceImplTest.java
@@ -24,6 +24,7 @@ import org.junit.Test;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TenantDeviceType;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceCredentialsId;
 import org.thingsboard.server.common.data.id.DeviceId;
@@ -37,6 +38,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executors;
 
 import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
 
@@ -65,6 +67,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         
         Assert.assertNotNull(savedDevice);
@@ -95,6 +98,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     @Test(expected = DataValidationException.class)
     public void testSaveDeviceWithEmptyName() {
         Device device = new Device();
+        device.setType("default");
         device.setTenantId(tenantId);
         deviceService.saveDevice(device);
     }
@@ -103,6 +107,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceWithEmptyTenant() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         deviceService.saveDevice(device);
     }
     
@@ -110,6 +115,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testSaveDeviceWithInvalidTenant() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(new TenantId(UUIDs.timeBased()));
         deviceService.saveDevice(device);
     }
@@ -118,6 +124,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testAssignDeviceToNonExistentCustomer() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         try {
@@ -131,6 +138,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
     public void testAssignDeviceToCustomerFromDifferentTenant() {
         Device device = new Device();
         device.setName("My device");
+        device.setType("default");
         device.setTenantId(tenantId);
         device = deviceService.saveDevice(device);
         Tenant tenant = new Tenant();
@@ -153,18 +161,56 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         Device foundDevice = deviceService.findDeviceById(savedDevice.getId());
         Assert.assertNotNull(foundDevice);
         Assert.assertEquals(savedDevice, foundDevice);
         deviceService.deleteDevice(savedDevice.getId());
     }
+
+    @Test
+    public void testFindDeviceTypesByTenantId() throws Exception {
+        List<Device> devices = new ArrayList<>();
+        try {
+            for (int i=0;i<3;i++) {
+                Device device = new Device();
+                device.setTenantId(tenantId);
+                device.setName("My device B"+i);
+                device.setType("typeB");
+                devices.add(deviceService.saveDevice(device));
+            }
+            for (int i=0;i<7;i++) {
+                Device device = new Device();
+                device.setTenantId(tenantId);
+                device.setName("My device C"+i);
+                device.setType("typeC");
+                devices.add(deviceService.saveDevice(device));
+            }
+            for (int i=0;i<9;i++) {
+                Device device = new Device();
+                device.setTenantId(tenantId);
+                device.setName("My device A"+i);
+                device.setType("typeA");
+                devices.add(deviceService.saveDevice(device));
+            }
+            List<TenantDeviceType> deviceTypes = deviceService.findDeviceTypesByTenantId(tenantId).get();
+            Assert.assertNotNull(deviceTypes);
+            Assert.assertEquals(3, deviceTypes.size());
+            Assert.assertEquals("typeA", deviceTypes.get(0).getType());
+            Assert.assertEquals("typeB", deviceTypes.get(1).getType());
+            Assert.assertEquals("typeC", deviceTypes.get(2).getType());
+        } finally {
+            devices.forEach((device) -> { deviceService.deleteDevice(device.getId()); });
+        }
+    }
     
     @Test
     public void testDeleteDevice() {
         Device device = new Device();
         device.setTenantId(tenantId);
         device.setName("My device");
+        device.setType("default");
         Device savedDevice = deviceService.saveDevice(device);
         Device foundDevice = deviceService.findDeviceById(savedDevice.getId());
         Assert.assertNotNull(foundDevice);
@@ -188,6 +234,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             Device device = new Device();
             device.setTenantId(tenantId);
             device.setName("Device"+i);
+            device.setType("default");
             devices.add(deviceService.saveDevice(device));
         }
         
@@ -216,7 +263,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         
         tenantService.deleteTenant(tenantId);
     }
-    
+
     @Test
     public void testFindDevicesByTenantIdAndName() {
         String title1 = "Device title 1";
@@ -228,6 +275,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle1.add(deviceService.saveDevice(device));
         }
         String title2 = "Device title 2";
@@ -239,6 +287,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             devicesTitle2.add(deviceService.saveDevice(device));
         }
         
@@ -291,6 +340,85 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Assert.assertFalse(pageData.hasNext());
         Assert.assertEquals(0, pageData.getData().size());
     }
+
+    @Test
+    public void testFindDevicesByTenantIdAndType() {
+        String title1 = "Device title 1";
+        String type1 = "typeA";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            devicesType1.add(deviceService.saveDevice(device));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeB";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            devicesType2.add(deviceService.saveDevice(device));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type1, pageLink);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type2, pageLink);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndType(tenantId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
     
     @Test
     public void testFindDevicesByTenantIdAndCustomerId() {
@@ -311,6 +439,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             Device device = new Device();
             device.setTenantId(tenantId);
             device.setName("Device"+i);
+            device.setType("default");
             device = deviceService.saveDevice(device);
             devices.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
         }
@@ -359,6 +488,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title1+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = deviceService.saveDevice(device);
             devicesTitle1.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
         }
@@ -371,6 +501,7 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
             String name = title2+suffix;
             name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
             device.setName(name);
+            device.setType("default");
             device = deviceService.saveDevice(device);
             devicesTitle2.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
         }
@@ -425,4 +556,94 @@ public class DeviceServiceImplTest extends AbstractServiceTest {
         Assert.assertEquals(0, pageData.getData().size());
         customerService.deleteCustomer(customerId);
     }
+
+    @Test
+    public void testFindDevicesByTenantIdCustomerIdAndType() {
+
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer.setTenantId(tenantId);
+        customer = customerService.saveCustomer(customer);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Device title 1";
+        String type1 = "typeC";
+        List<Device> devicesType1 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type1);
+            device = deviceService.saveDevice(device);
+            devicesType1.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
+        }
+        String title2 = "Device title 2";
+        String type2 = "typeD";
+        List<Device> devicesType2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            device.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device.setType(type2);
+            device = deviceService.saveDevice(device);
+            devicesType2.add(deviceService.assignDeviceToCustomer(device.getId(), customerId));
+        }
+
+        List<Device> loadedDevicesType1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+            loadedDevicesType1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType1, idComparator);
+        Collections.sort(loadedDevicesType1, idComparator);
+
+        Assert.assertEquals(devicesType1, loadedDevicesType1);
+
+        List<Device> loadedDevicesType2 = new ArrayList<>();
+        pageLink = new TextPageLink(4);
+        do {
+            pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+            loadedDevicesType2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesType2, idComparator);
+        Collections.sort(loadedDevicesType2, idComparator);
+
+        Assert.assertEquals(devicesType2, loadedDevicesType2);
+
+        for (Device device : loadedDevicesType1) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type1, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+
+        for (Device device : loadedDevicesType2) {
+            deviceService.deleteDevice(device.getId());
+        }
+
+        pageLink = new TextPageLink(4);
+        pageData = deviceService.findDevicesByTenantIdAndCustomerIdAndType(tenantId, customerId, type2, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        customerService.deleteCustomer(customerId);
+    }
+
 }
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
index 3f5c55a..f7e47ce 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceImplTest.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.relation.EntityRelationsQuery;
 import org.thingsboard.server.dao.relation.EntitySearchDirection;
@@ -55,13 +56,13 @@ public class RelationServiceImplTest extends AbstractServiceTest {
 
         Assert.assertTrue(saveRelation(relation));
 
-        Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE").get());
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE").get());
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
     }
 
     @Test
@@ -78,11 +79,11 @@ public class RelationServiceImplTest extends AbstractServiceTest {
 
         Assert.assertTrue(relationService.deleteRelation(relationA).get());
 
-        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
     }
 
     @Test
@@ -99,9 +100,9 @@ public class RelationServiceImplTest extends AbstractServiceTest {
 
         Assert.assertTrue(relationService.deleteEntityRelations(childId).get());
 
-        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
 
-        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
     }
 
     @Test
@@ -123,7 +124,7 @@ public class RelationServiceImplTest extends AbstractServiceTest {
         saveRelation(relationB1);
         saveRelation(relationB2);
 
-        List<EntityRelation> relations = relationService.findByFrom(parentA).get();
+        List<EntityRelation> relations = relationService.findByFrom(parentA, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(EntityRelation.CONTAINS_TYPE, relation.getType());
@@ -131,13 +132,13 @@ public class RelationServiceImplTest extends AbstractServiceTest {
             Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
         }
 
-        relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
 
-        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByFrom(parentB).get();
+        relations = relationService.findByFrom(parentB, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(EntityRelation.MANAGES_TYPE, relation.getType());
@@ -145,10 +146,10 @@ public class RelationServiceImplTest extends AbstractServiceTest {
             Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
         }
 
-        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
     }
 
@@ -178,26 +179,26 @@ public class RelationServiceImplTest extends AbstractServiceTest {
         // Data propagation to views is async
         Thread.sleep(3000);
 
-        List<EntityRelation> relations = relationService.findByTo(childA).get();
+        List<EntityRelation> relations = relationService.findByTo(childA, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(childA, relation.getTo());
             Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom()));
         }
 
-        relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE).get();
+        relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(1, relations.size());
 
-        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(1, relations.size());
 
-        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE).get();
+        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(0, relations.size());
 
-        relations = relationService.findByTo(childB).get();
+        relations = relationService.findByTo(childB, RelationTypeGroup.COMMON).get();
         Assert.assertEquals(2, relations.size());
         for (EntityRelation relation : relations) {
             Assert.assertEquals(childB, relation.getTo());
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
new file mode 100644
index 0000000..8a082fc
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
@@ -0,0 +1,284 @@
+/**
+ * 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.dao.service;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.dao.exception.DataValidationException;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
+import org.thingsboard.server.dao.relation.EntityTypeFilter;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+public class RelationServiceTest extends AbstractServiceTest {
+
+    @Before
+    public void before() {
+    }
+
+    @After
+    public void after() {
+    }
+
+    @Test
+    public void testSaveRelation() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+
+        Assert.assertTrue(saveRelation(relation));
+
+        Assert.assertTrue(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE", RelationTypeGroup.COMMON).get());
+    }
+
+    @Test
+    public void testDeleteRelation() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+        AssetId subChildId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
+
+        saveRelation(relationA);
+        saveRelation(relationB);
+
+        Assert.assertTrue(relationService.deleteRelation(relationA).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+    }
+
+    @Test
+    public void testDeleteEntityRelations() throws ExecutionException, InterruptedException {
+        AssetId parentId = new AssetId(UUIDs.timeBased());
+        AssetId childId = new AssetId(UUIDs.timeBased());
+        AssetId subChildId = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationB = new EntityRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE);
+
+        saveRelation(relationA);
+        saveRelation(relationB);
+
+        Assert.assertTrue(relationService.deleteEntityRelations(childId).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
+    }
+
+    @Test
+    public void testFindFrom() throws ExecutionException, InterruptedException {
+        AssetId parentA = new AssetId(UUIDs.timeBased());
+        AssetId parentB = new AssetId(UUIDs.timeBased());
+        AssetId childA = new AssetId(UUIDs.timeBased());
+        AssetId childB = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE);
+
+        EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE);
+        EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE);
+
+        saveRelation(relationA1);
+        saveRelation(relationA2);
+
+        saveRelation(relationB1);
+        saveRelation(relationB2);
+
+        List<EntityRelation> relations = relationService.findByFrom(parentA, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(EntityRelation.CONTAINS_TYPE, relation.getType());
+            Assert.assertEquals(parentA, relation.getFrom());
+            Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
+        }
+
+        relations = relationService.findByFromAndType(parentA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+
+        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFrom(parentB, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(EntityRelation.MANAGES_TYPE, relation.getType());
+            Assert.assertEquals(parentB, relation.getFrom());
+            Assert.assertTrue(childA.equals(relation.getTo()) || childB.equals(relation.getTo()));
+        }
+
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+    }
+
+    private Boolean saveRelation(EntityRelation relationA1) throws ExecutionException, InterruptedException {
+        return relationService.saveRelation(relationA1).get();
+    }
+
+    @Test
+    public void testFindTo() throws ExecutionException, InterruptedException {
+        AssetId parentA = new AssetId(UUIDs.timeBased());
+        AssetId parentB = new AssetId(UUIDs.timeBased());
+        AssetId childA = new AssetId(UUIDs.timeBased());
+        AssetId childB = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA1 = new EntityRelation(parentA, childA, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationA2 = new EntityRelation(parentA, childB, EntityRelation.CONTAINS_TYPE);
+
+        EntityRelation relationB1 = new EntityRelation(parentB, childA, EntityRelation.MANAGES_TYPE);
+        EntityRelation relationB2 = new EntityRelation(parentB, childB, EntityRelation.MANAGES_TYPE);
+
+        saveRelation(relationA1);
+        saveRelation(relationA2);
+
+        saveRelation(relationB1);
+        saveRelation(relationB2);
+
+        // Data propagation to views is async
+        Thread.sleep(3000);
+
+        List<EntityRelation> relations = relationService.findByTo(childA, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(childA, relation.getTo());
+            Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom()));
+        }
+
+        relations = relationService.findByToAndType(childA, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByTo(childB, RelationTypeGroup.COMMON).get();
+        Assert.assertEquals(2, relations.size());
+        for (EntityRelation relation : relations) {
+            Assert.assertEquals(childB, relation.getTo());
+            Assert.assertTrue(parentA.equals(relation.getFrom()) || parentB.equals(relation.getFrom()));
+        }
+    }
+
+    @Test
+    public void testCyclicRecursiveRelation() throws ExecutionException, InterruptedException {
+        // A -> B -> C -> A
+        AssetId assetA = new AssetId(UUIDs.timeBased());
+        AssetId assetB = new AssetId(UUIDs.timeBased());
+        AssetId assetC = new AssetId(UUIDs.timeBased());
+
+        EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationC = new EntityRelation(assetC, assetA, EntityRelation.CONTAINS_TYPE);
+
+        saveRelation(relationA);
+        saveRelation(relationB);
+        saveRelation(relationC);
+
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1));
+        query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
+        List<EntityRelation> relations = relationService.findByQuery(query).get();
+        Assert.assertEquals(3, relations.size());
+        Assert.assertTrue(relations.contains(relationA));
+        Assert.assertTrue(relations.contains(relationB));
+        Assert.assertTrue(relations.contains(relationC));
+    }
+
+    @Test
+    public void testRecursiveRelation() throws ExecutionException, InterruptedException {
+        // A -> B -> [C,D]
+        AssetId assetA = new AssetId(UUIDs.timeBased());
+        AssetId assetB = new AssetId(UUIDs.timeBased());
+        AssetId assetC = new AssetId(UUIDs.timeBased());
+        DeviceId deviceD = new DeviceId(UUIDs.timeBased());
+
+        EntityRelation relationAB = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationBC = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE);
+        EntityRelation relationBD = new EntityRelation(assetB, deviceD, EntityRelation.CONTAINS_TYPE);
+
+
+        saveRelation(relationAB);
+        saveRelation(relationBC);
+        saveRelation(relationBD);
+
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1));
+        query.setFilters(Collections.singletonList(new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
+        List<EntityRelation> relations = relationService.findByQuery(query).get();
+        Assert.assertEquals(2, relations.size());
+        Assert.assertTrue(relations.contains(relationAB));
+        Assert.assertTrue(relations.contains(relationBC));
+    }
+
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException {
+        EntityRelation relation = new EntityRelation();
+        relation.setTo(new AssetId(UUIDs.timeBased()));
+        relation.setType(EntityRelation.CONTAINS_TYPE);
+        Assert.assertTrue(saveRelation(relation));
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException {
+        EntityRelation relation = new EntityRelation();
+        relation.setFrom(new AssetId(UUIDs.timeBased()));
+        relation.setType(EntityRelation.CONTAINS_TYPE);
+        Assert.assertTrue(saveRelation(relation));
+    }
+
+    @Test(expected = DataValidationException.class)
+    public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException {
+        EntityRelation relation = new EntityRelation();
+        relation.setFrom(new AssetId(UUIDs.timeBased()));
+        relation.setTo(new AssetId(UUIDs.timeBased()));
+        Assert.assertTrue(saveRelation(relation));
+    }
+}

docker/.env 6(+6 -0)

diff --git a/docker/.env b/docker/.env
index ca7f2b0..1c7e512 100644
--- a/docker/.env
+++ b/docker/.env
@@ -1 +1,7 @@
 CASSANDRA_DATA_DIR=/home/docker/cassandra_volume
+
+# cassandra schema container environment variables
+CREATE_SCHEMA=true
+ADD_SYSTEM_DATA=false
+ADD_DEMO_DATA=false
+CASSANDRA_URL=cassandra
\ No newline at end of file
diff --git a/docker/cassandra/cassandra.yaml b/docker/cassandra/cassandra.yaml
new file mode 100644
index 0000000..6b4857f
--- /dev/null
+++ b/docker/cassandra/cassandra.yaml
@@ -0,0 +1,132 @@
+#
+# 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.
+#
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: cassandra-headless
+  labels:
+    app: cassandra-headless
+spec:
+  ports:
+    - port: 9042
+      name: cql
+  clusterIP: None
+  selector:
+    app: cassandra
+---
+apiVersion: "apps/v1beta1"
+kind: StatefulSet
+metadata:
+  name: cassandra
+spec:
+  serviceName: cassandra-headless
+  replicas: 2
+  template:
+    metadata:
+      labels:
+        app: cassandra
+    spec:
+      nodeSelector:
+        machinetype: other
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "app"
+                    operator: In
+                    values:
+                    - cassandra-headless
+              topologyKey: "kubernetes.io/hostname"
+      containers:
+      - name: cassandra
+        image: thingsboard/cassandra:1.2.4
+        imagePullPolicy: Always
+        ports:
+        - containerPort: 7000
+          name: intra-node
+        - containerPort: 7001
+          name: tls-intra-node
+        - containerPort: 7199
+          name: jmx
+        - containerPort: 9042
+          name: cql
+        - containerPort: 9160
+          name: thrift
+        securityContext:
+          capabilities:
+            add:
+              - IPC_LOCK
+        lifecycle:
+          preStop:
+            exec:
+              command: ["/bin/sh", "-c", "PID=$(pidof java) && kill $PID && while ps -p $PID > /dev/null; do sleep 1; done"]
+        env:
+          - name: MAX_HEAP_SIZE
+            value: 2048M
+          - name: HEAP_NEWSIZE
+            value: 100M
+          - name: CASSANDRA_SEEDS
+            value: "cassandra-0.cassandra-headless.default.svc.cluster.local"
+          - name: CASSANDRA_CLUSTER_NAME
+            value: "Thingsboard-Cluster"
+          - name: CASSANDRA_DC
+            value: "DC1-Thingsboard-Cluster"
+          - name: CASSANDRA_RACK
+            value: "Rack-Thingsboard-Cluster"
+          - name: CASSANDRA_AUTO_BOOTSTRAP
+            value: "false"
+          - name: POD_IP
+            valueFrom:
+              fieldRef:
+                fieldPath: status.podIP
+          - name: POD_NAMESPACE
+            valueFrom:
+              fieldRef:
+                fieldPath: metadata.namespace
+        readinessProbe:
+          exec:
+            command:
+            - /bin/bash
+            - -c
+            - /ready-probe.sh
+          initialDelaySeconds: 15
+          timeoutSeconds: 5
+        volumeMounts:
+        - name: cassandra-data
+          mountPath: /var/lib/cassandra/data
+        - name: cassandra-commitlog
+          mountPath: /var/lib/cassandra/commitlog
+  volumeClaimTemplates:
+  - metadata:
+      name: cassandra-data
+      annotations:
+        volume.beta.kubernetes.io/storage-class: fast
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      resources:
+        requests:
+          storage: 3Gi
+  - metadata:
+      name: cassandra-commitlog
+      annotations:
+        volume.beta.kubernetes.io/storage-class: fast
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      resources:
+        requests:
+          storage: 2Gi
\ No newline at end of file
diff --git a/docker/cassandra/Makefile b/docker/cassandra/Makefile
new file mode 100644
index 0000000..15eb3ed
--- /dev/null
+++ b/docker/cassandra/Makefile
@@ -0,0 +1,10 @@
+VERSION=1.2.4
+PROJECT=thingsboard
+APP=cassandra
+
+build:
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} -t ${PROJECT}/${APP}:latest .
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
+    docker push ${PROJECT}/${APP}:latest
\ No newline at end of file
diff --git a/docker/docker-compose.static.yml b/docker/docker-compose.static.yml
index 80cc6a9..5471cb2 100644
--- a/docker/docker-compose.static.yml
+++ b/docker/docker-compose.static.yml
@@ -17,7 +17,7 @@
 version: '2'
 
 services:
-  db:
+  cassandra:
     ports:
       - "9042:9042"
       - "9160:9160"
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 97873e8..8367abc 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -17,24 +17,32 @@
 version: '2'
 
 services:
-  thingsboard:
-    image: "thingsboard/application:1.2.3"
+  tb:
+    image: "thingsboard/application:1.2.4"
     ports:
       - "8080:8080"
       - "1883:1883"
       - "5683:5683/udp"
     env_file:
-      - thingsboard.env
-    entrypoint: ./run_thingsboard.sh
-  thingsboard-db-schema:
-    image: "thingsboard/thingsboard-db-schema:1.2.3"
-    env_file:
-      - thingsboard-db-schema.env
-    entrypoint: ./install_schema.sh
-  db:
+      - tb.env
+    entrypoint: ./run-application.sh
+  tb-cassandra-schema:
+    image: "thingsboard/tb-cassandra-schema:1.2.4"
+    environment:
+      - CREATE_SCHEMA=${CREATE_SCHEMA}
+      - ADD_SYSTEM_DATA=${ADD_SYSTEM_DATA}
+      - ADD_DEMO_DATA=${ADD_DEMO_DATA}
+      - CASSANDRA_URL=${CASSANDRA_URL}
+    entrypoint: ./install-schema.sh
+  cassandra:
     image: "cassandra:3.9"
+    ports:
+      - "9042"
+      - "9160"
     volumes:
       - "${CASSANDRA_DATA_DIR}:/var/lib/cassandra"
   zk:
     image: "zookeeper:3.4.9"
+    ports:
+      - "2181"
     restart: always

docker/tb/Makefile 12(+12 -0)

diff --git a/docker/tb/Makefile b/docker/tb/Makefile
new file mode 100644
index 0000000..0d38e51
--- /dev/null
+++ b/docker/tb/Makefile
@@ -0,0 +1,12 @@
+VERSION=1.2.4
+PROJECT=thingsboard
+APP=application
+
+build:
+	cp ../../application/target/thingsboard.deb .
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} -t ${PROJECT}/${APP}:latest .
+	rm thingsboard.deb
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
+    docker push ${PROJECT}/${APP}:latest
\ No newline at end of file

docker/tb/tb.yaml 121(+121 -0)

diff --git a/docker/tb/tb.yaml b/docker/tb/tb.yaml
new file mode 100644
index 0000000..15ed193
--- /dev/null
+++ b/docker/tb/tb.yaml
@@ -0,0 +1,121 @@
+#
+# 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.
+#
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: tb-service
+  labels:
+    app: tb-service
+spec:
+  ports:
+  - port: 8080
+    name: ui
+  - port: 1883
+    name: mqtt
+  - port: 5683
+    name: coap
+  selector:
+    app: tb
+  type: LoadBalancer
+---
+apiVersion: policy/v1beta1
+kind: PodDisruptionBudget
+metadata:
+  name: tb-budget
+spec:
+  selector:
+    matchLabels:
+      app: tb
+  minAvailable: 3
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: tb-config
+data:
+  zookeeper.enabled: "true"
+  zookeeper.url: "zk-headless"
+  cassandra.url: "cassandra-headless:9042"
+---
+apiVersion: apps/v1beta1
+kind: StatefulSet
+metadata:
+  name: tb
+spec:
+  serviceName: "tb-service"
+  replicas: 3
+  template:
+    metadata:
+      labels:
+        app: tb
+    spec:
+      nodeSelector:
+        machinetype: tb
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "app"
+                    operator: In
+                    values:
+                    - tb-service
+              topologyKey: "kubernetes.io/hostname"
+      containers:
+      - name: tb
+        imagePullPolicy: Always
+        image: thingsboard/application:1.2.4
+        ports:
+        - containerPort: 8080
+          name: ui
+        - containerPort: 1883
+          name: mqtt
+        - containerPort: 5683
+          name: coap
+        - containerPort: 9001
+          name: rpc
+        env:
+        - name: ZOOKEEPER_ENABLED
+          valueFrom:
+            configMapKeyRef:
+              name: tb-config
+              key: zookeeper.enabled
+        - name: ZOOKEEPER_URL
+          valueFrom:
+            configMapKeyRef:
+              name: tb-config
+              key: zookeeper.url
+        - name : CASSANDRA_URL
+          valueFrom:
+            configMapKeyRef:
+              name: tb-config
+              key: cassandra.url
+        - name : RPC_HOST
+          valueFrom:
+            fieldRef:
+              fieldPath: status.podIP
+        command:
+        - sh
+        - -c
+        - ./run-application.sh
+        livenessProbe:
+          httpGet:
+            path: /login
+            port: ui-port
+          initialDelaySeconds: 120
+          timeoutSeconds: 10
\ No newline at end of file
diff --git a/docker/tb-cassandra-schema/Makefile b/docker/tb-cassandra-schema/Makefile
new file mode 100644
index 0000000..68625f4
--- /dev/null
+++ b/docker/tb-cassandra-schema/Makefile
@@ -0,0 +1,14 @@
+VERSION=1.2.4
+PROJECT=thingsboard
+APP=tb-cassandra-schema
+
+build:
+	cp ../../dao/src/main/resources/schema.cql .
+	cp ../../dao/src/main/resources/demo-data.cql .
+	cp ../../dao/src/main/resources/system-data.cql .
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} -t ${PROJECT}/${APP}:latest .
+	rm schema.cql demo-data.cql system-data.cql
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
+    docker push ${PROJECT}/${APP}:latest
\ No newline at end of file
diff --git a/docker/tb-cassandra-schema/tb-cassandra-schema.yaml b/docker/tb-cassandra-schema/tb-cassandra-schema.yaml
new file mode 100644
index 0000000..7b46a2e
--- /dev/null
+++ b/docker/tb-cassandra-schema/tb-cassandra-schema.yaml
@@ -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.
+#
+
+apiVersion: v1
+kind: Pod
+metadata:
+  name: tb-cassandra-schema
+spec:
+  containers:
+  - name: tb-cassandra-schema
+    imagePullPolicy: Always
+    image: thingsboard/tb-cassandra-schema:1.2.4
+    env:
+    - name: CREATE_SCHEMA
+      value: "true"
+    - name: ADD_SYSTEM_DATA
+      value: "true"
+    - name : ADD_DEMO_DATA
+      value: "true"
+    - name : CASSANDRA_URL
+      value: "cassandra-headless"
+    command:
+    - sh
+    - -c
+    - ./install-schema.sh
+  restartPolicy: Never
\ No newline at end of file
diff --git a/docker/zookeeper/Dockerfile b/docker/zookeeper/Dockerfile
new file mode 100644
index 0000000..5ec1ac9
--- /dev/null
+++ b/docker/zookeeper/Dockerfile
@@ -0,0 +1,71 @@
+#
+# 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.
+#
+
+FROM ubuntu:16.04 
+ENV ZK_USER=zookeeper \
+ZK_DATA_DIR=/var/lib/zookeeper/data \
+ZK_DATA_LOG_DIR=/var/lib/zookeeper/log \
+ZK_LOG_DIR=/var/log/zookeeper \
+JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
+
+ARG GPG_KEY=C823E3E5B12AF29C67F81976F5CECB3CB5E9BD2D
+ARG ZK_DIST=zookeeper-3.4.9
+RUN set -x \
+    && apt-get update \
+    && apt-get install -y openjdk-8-jre-headless wget netcat-openbsd \
+	&& wget -q "http://www.apache.org/dist/zookeeper/$ZK_DIST/$ZK_DIST.tar.gz" \
+    && wget -q "http://www.apache.org/dist/zookeeper/$ZK_DIST/$ZK_DIST.tar.gz.asc" \
+    && export GNUPGHOME="$(mktemp -d)" \
+    && gpg --keyserver ha.pool.sks-keyservers.net --recv-key "$GPG_KEY" \
+    && gpg --batch --verify "$ZK_DIST.tar.gz.asc" "$ZK_DIST.tar.gz" \
+    && tar -xzf "$ZK_DIST.tar.gz" -C /opt \
+    && rm -r "$GNUPGHOME" "$ZK_DIST.tar.gz" "$ZK_DIST.tar.gz.asc" \
+    && ln -s /opt/$ZK_DIST /opt/zookeeper \
+    && rm -rf /opt/zookeeper/CHANGES.txt \
+    /opt/zookeeper/README.txt \
+    /opt/zookeeper/NOTICE.txt \
+    /opt/zookeeper/CHANGES.txt \
+    /opt/zookeeper/README_packaging.txt \
+    /opt/zookeeper/build.xml \
+    /opt/zookeeper/config \
+    /opt/zookeeper/contrib \
+    /opt/zookeeper/dist-maven \
+    /opt/zookeeper/docs \
+    /opt/zookeeper/ivy.xml \
+    /opt/zookeeper/ivysettings.xml \
+    /opt/zookeeper/recipes \
+    /opt/zookeeper/src \
+    /opt/zookeeper/$ZK_DIST.jar.asc \
+    /opt/zookeeper/$ZK_DIST.jar.md5 \
+    /opt/zookeeper/$ZK_DIST.jar.sha1 \
+	&& apt-get autoremove -y wget \
+	&& rm -rf /var/lib/apt/lists/*
+
+#Copy configuration generator script to bin
+COPY zk-gen-config.sh zk-ok.sh /opt/zookeeper/bin/
+
+# Create a user for the zookeeper process and configure file system ownership 
+# for nessecary directories and symlink the distribution as a user executable
+RUN set -x \
+	&& useradd $ZK_USER \
+    && [ `id -u $ZK_USER` -eq 1000 ] \
+    && [ `id -g $ZK_USER` -eq 1000 ] \
+    && mkdir -p $ZK_DATA_DIR $ZK_DATA_LOG_DIR $ZK_LOG_DIR /usr/share/zookeeper /tmp/zookeeper /usr/etc/ \
+	&& chown -R "$ZK_USER:$ZK_USER" /opt/$ZK_DIST $ZK_DATA_DIR $ZK_LOG_DIR $ZK_DATA_LOG_DIR /tmp/zookeeper \
+	&& ln -s /opt/zookeeper/conf/ /usr/etc/zookeeper \
+	&& ln -s /opt/zookeeper/bin/* /usr/bin \
+	&& ln -s /opt/zookeeper/$ZK_DIST.jar /usr/share/zookeeper/ \
+	&& ln -s /opt/zookeeper/lib/* /usr/share/zookeeper 
diff --git a/docker/zookeeper/Makefile b/docker/zookeeper/Makefile
new file mode 100644
index 0000000..911c4f3
--- /dev/null
+++ b/docker/zookeeper/Makefile
@@ -0,0 +1,10 @@
+VERSION=1.2.4
+PROJECT=thingsboard
+APP=zk
+
+build:
+	docker build --pull -t ${PROJECT}/${APP}:${VERSION} -t ${PROJECT}/${APP}:latest .
+
+push: build
+	docker push ${PROJECT}/${APP}:${VERSION}
+	docker push ${PROJECT}/${APP}:latest
\ No newline at end of file
diff --git a/docker/zookeeper/zk-gen-config.sh b/docker/zookeeper/zk-gen-config.sh
new file mode 100755
index 0000000..02fde70
--- /dev/null
+++ b/docker/zookeeper/zk-gen-config.sh
@@ -0,0 +1,153 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+ZK_USER=${ZK_USER:-"zookeeper"}
+ZK_LOG_LEVEL=${ZK_LOG_LEVEL:-"INFO"}
+ZK_DATA_DIR=${ZK_DATA_DIR:-"/var/lib/zookeeper/data"}
+ZK_DATA_LOG_DIR=${ZK_DATA_LOG_DIR:-"/var/lib/zookeeper/log"}
+ZK_LOG_DIR=${ZK_LOG_DIR:-"var/log/zookeeper"}
+ZK_CONF_DIR=${ZK_CONF_DIR:-"/opt/zookeeper/conf"}
+ZK_CLIENT_PORT=${ZK_CLIENT_PORT:-2181}
+ZK_SERVER_PORT=${ZK_SERVER_PORT:-2888}
+ZK_ELECTION_PORT=${ZK_ELECTION_PORT:-3888}
+ZK_TICK_TIME=${ZK_TICK_TIME:-2000}
+ZK_INIT_LIMIT=${ZK_INIT_LIMIT:-10}
+ZK_SYNC_LIMIT=${ZK_SYNC_LIMIT:-5}
+ZK_HEAP_SIZE=${ZK_HEAP_SIZE:-2G}
+ZK_MAX_CLIENT_CNXNS=${ZK_MAX_CLIENT_CNXNS:-60}
+ZK_MIN_SESSION_TIMEOUT=${ZK_MIN_SESSION_TIMEOUT:- $((ZK_TICK_TIME*2))}
+ZK_MAX_SESSION_TIMEOUT=${ZK_MAX_SESSION_TIMEOUT:- $((ZK_TICK_TIME*20))}
+ZK_SNAP_RETAIN_COUNT=${ZK_SNAP_RETAIN_COUNT:-3}
+ZK_PURGE_INTERVAL=${ZK_PURGE_INTERVAL:-0}
+ID_FILE="$ZK_DATA_DIR/myid"
+ZK_CONFIG_FILE="$ZK_CONF_DIR/zoo.cfg"
+LOGGER_PROPS_FILE="$ZK_CONF_DIR/log4j.properties"
+JAVA_ENV_FILE="$ZK_CONF_DIR/java.env"
+HOST=`hostname -s`
+DOMAIN=`hostname -d`
+
+function print_servers() {
+	 for (( i=1; i<=$ZK_REPLICAS; i++ ))
+	do
+		echo "server.$i=$NAME-$((i-1)).$DOMAIN:$ZK_SERVER_PORT:$ZK_ELECTION_PORT"
+	done
+}
+
+function validate_env() {
+    echo "Validating environment"
+	if [ -z $ZK_REPLICAS ]; then
+		echo "ZK_REPLICAS is a mandatory environment variable"
+		exit 1
+	fi
+
+	if [[ $HOST =~ (.*)-([0-9]+)$ ]]; then
+		NAME=${BASH_REMATCH[1]}
+		ORD=${BASH_REMATCH[2]}
+	else
+		echo "Failed to extract ordinal from hostname $HOST"
+		exit 1
+	fi
+	MY_ID=$((ORD+1))
+	echo "ZK_REPLICAS=$ZK_REPLICAS"
+    echo "MY_ID=$MY_ID"
+    echo "ZK_LOG_LEVEL=$ZK_LOG_LEVEL"
+    echo "ZK_DATA_DIR=$ZK_DATA_DIR"
+    echo "ZK_DATA_LOG_DIR=$ZK_DATA_LOG_DIR"
+    echo "ZK_LOG_DIR=$ZK_LOG_DIR"
+    echo "ZK_CLIENT_PORT=$ZK_CLIENT_PORT"
+    echo "ZK_SERVER_PORT=$ZK_SERVER_PORT"
+    echo "ZK_ELECTION_PORT=$ZK_ELECTION_PORT"
+    echo "ZK_TICK_TIME=$ZK_TICK_TIME"
+    echo "ZK_INIT_LIMIT=$ZK_INIT_LIMIT"
+    echo "ZK_SYNC_LIMIT=$ZK_SYNC_LIMIT"
+    echo "ZK_MAX_CLIENT_CNXNS=$ZK_MAX_CLIENT_CNXNS"
+    echo "ZK_MIN_SESSION_TIMEOUT=$ZK_MIN_SESSION_TIMEOUT"
+    echo "ZK_MAX_SESSION_TIMEOUT=$ZK_MAX_SESSION_TIMEOUT"
+    echo "ZK_HEAP_SIZE=$ZK_HEAP_SIZE"
+    echo "ZK_SNAP_RETAIN_COUNT=$ZK_SNAP_RETAIN_COUNT"
+    echo "ZK_PURGE_INTERVAL=$ZK_PURGE_INTERVAL"
+    echo "ENSEMBLE"
+    print_servers
+    echo "Environment validation successful"
+}
+
+function create_config() {
+	rm -f $ZK_CONFIG_FILE
+    echo "Creating ZooKeeper configuration"
+    echo "#This file was autogenerated by zk DO NOT EDIT" >> $ZK_CONFIG_FILE
+	echo "clientPort=$ZK_CLIENT_PORT" >> $ZK_CONFIG_FILE
+    echo "dataDir=$ZK_DATA_DIR" >> $ZK_CONFIG_FILE
+    echo "dataLogDir=$ZK_DATA_LOG_DIR" >> $ZK_CONFIG_FILE
+    echo "tickTime=$ZK_TICK_TIME" >> $ZK_CONFIG_FILE
+    echo "initLimit=$ZK_INIT_LIMIT" >> $ZK_CONFIG_FILE
+    echo "syncLimit=$ZK_SYNC_LIMIT" >> $ZK_CONFIG_FILE
+    echo "maxClientCnxns=$ZK_MAX_CLIENT_CNXNS" >> $ZK_CONFIG_FILE
+    echo "minSessionTimeout=$ZK_MIN_SESSION_TIMEOUT" >> $ZK_CONFIG_FILE
+    echo "maxSessionTimeout=$ZK_MAX_SESSION_TIMEOUT" >> $ZK_CONFIG_FILE
+    echo "autopurge.snapRetainCount=$ZK_SNAP_RETAIN_COUNT" >> $ZK_CONFIG_FILE
+    echo "autopurge.purgeInteval=$ZK_PURGE_INTERVAL" >> $ZK_CONFIG_FILE
+
+    if [ $ZK_REPLICAS -gt 1 ]; then
+    	print_servers >> $ZK_CONFIG_FILE
+    fi
+    echo "Wrote ZooKeeper configuration file to $ZK_CONFIG_FILE"
+}
+
+function create_data_dirs() {
+	echo "Creating ZooKeeper data directories and setting permissions"
+    if [ ! -d $ZK_DATA_DIR  ]; then
+        mkdir -p $ZK_DATA_DIR
+        chown -R $ZK_USER:$ZK_USER $ZK_DATA_DIR
+    fi
+
+    if [ ! -d $ZK_DATA_LOG_DIR  ]; then
+        mkdir -p $ZK_DATA_LOG_DIR
+        chown -R $ZK_USER:$ZK_USER $ZK_DATA_LOG_DIR
+    fi
+
+    if [ ! -d $ZK_LOG_DIR  ]; then
+        mkdir -p $ZK_LOG_DIR
+        chown -R $ZK_USER:$ZK_USER $ZK_LOG_DIR
+    fi
+    if [ ! -f $ID_FILE ]; then
+        echo $MY_ID >> $ID_FILE
+    fi
+    echo "Created ZooKeeper data directories and set permissions in $ZK_DATA_DIR"
+}
+
+function create_log_props () {
+	rm -f $LOGGER_PROPS_FILE
+    echo "Creating ZooKeeper log4j configuration"
+	echo "zookeeper.root.logger=CONSOLE" >> $LOGGER_PROPS_FILE
+	echo "zookeeper.console.threshold="$ZK_LOG_LEVEL >> $LOGGER_PROPS_FILE
+	echo "log4j.rootLogger=\${zookeeper.root.logger}" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE.Threshold=\${zookeeper.console.threshold}" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout" >> $LOGGER_PROPS_FILE
+	echo "log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n" >> $LOGGER_PROPS_FILE
+	echo "Wrote log4j configuration to $LOGGER_PROPS_FILE"
+}
+
+function create_java_env() {
+    rm -f $JAVA_ENV_FILE
+    echo "Creating JVM configuration file"
+    echo "ZOO_LOG_DIR=$ZK_LOG_DIR" >> $JAVA_ENV_FILE
+    echo "JVMFLAGS=\"-Xmx$ZK_HEAP_SIZE -Xms$ZK_HEAP_SIZE\"" >> $JAVA_ENV_FILE
+    echo "Wrote JVM configuration to $JAVA_ENV_FILE"
+}
+
+validate_env && create_config && create_log_props && create_data_dirs && create_java_env
diff --git a/docker/zookeeper/zookeeper.yaml b/docker/zookeeper/zookeeper.yaml
new file mode 100644
index 0000000..d96a744
--- /dev/null
+++ b/docker/zookeeper/zookeeper.yaml
@@ -0,0 +1,190 @@
+#
+# 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.
+#
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: zk-headless
+  labels:
+    app: zk-headless
+spec:
+  ports:
+  - port: 2888
+    name: server
+  - port: 3888
+    name: leader-election
+  clusterIP: None
+  selector:
+    app: zk
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: zk-config
+data:
+  ensemble: "zk-0;zk-1;zk-2"
+  replicas: "3"
+  jvm.heap: "500m"
+  tick: "2000"
+  init: "10"
+  sync: "5"
+  client.cnxns: "60"
+  snap.retain: "3"
+  purge.interval: "1"
+  client.port: "2181"
+  server.port: "2888"
+  election.port: "3888"
+---
+apiVersion: policy/v1beta1
+kind: PodDisruptionBudget
+metadata:
+  name: zk-budget
+spec:
+  selector:
+    matchLabels:
+      app: zk
+  minAvailable: 3
+---
+apiVersion: apps/v1beta1
+kind: StatefulSet
+metadata:
+  name: zk
+spec:
+  serviceName: zk-headless
+  replicas: 3
+  template:
+    metadata:
+      labels:
+        app: zk
+      annotations:
+        pod.alpha.kubernetes.io/initialized: "true"
+    spec:
+      nodeSelector:
+        machinetype: other
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "app"
+                    operator: In
+                    values:
+                    - zk-headless
+              topologyKey: "kubernetes.io/hostname"
+      containers:
+      - name: zk
+        imagePullPolicy: Always
+        image: thingsboard/zk:1.2.4
+        ports:
+        - containerPort: 2181
+          name: client
+        - containerPort: 2888
+          name: server
+        - containerPort: 3888
+          name: leader-election
+        env:
+        - name : ZK_ENSEMBLE
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: ensemble
+        - name : ZK_REPLICAS
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: replicas
+        - name : ZK_HEAP_SIZE
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: jvm.heap
+        - name : ZK_TICK_TIME
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: tick
+        - name : ZK_INIT_LIMIT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: init
+        - name : ZK_SYNC_LIMIT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: tick
+        - name : ZK_MAX_CLIENT_CNXNS
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: client.cnxns
+        - name: ZK_SNAP_RETAIN_COUNT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: snap.retain
+        - name: ZK_PURGE_INTERVAL
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: purge.interval
+        - name: ZK_CLIENT_PORT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: client.port
+        - name: ZK_SERVER_PORT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: server.port
+        - name: ZK_ELECTION_PORT
+          valueFrom:
+            configMapKeyRef:
+              name: zk-config
+              key: election.port
+        command:
+        - sh
+        - -c
+        - zk-gen-config.sh && zkServer.sh start-foreground
+        readinessProbe:
+          exec:
+            command:
+            - "zk-ok.sh"
+          initialDelaySeconds: 15
+          timeoutSeconds: 5
+        livenessProbe:
+          exec:
+            command:
+            - "zk-ok.sh"
+          initialDelaySeconds: 15
+          timeoutSeconds: 5
+        volumeMounts:
+        - name: zkdatadir
+          mountPath: /var/lib/zookeeper
+      securityContext:
+        runAsUser: 1000
+        fsGroup: 1000
+  volumeClaimTemplates:
+  - metadata:
+      name: zkdatadir
+      annotations:
+        volume.beta.kubernetes.io/storage-class: slow
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      resources:
+        requests:
+          storage: 1Gi
\ No newline at end of file

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 8a2b0ce..81df515 100755
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
         <guava.version>18.0</guava.version>
         <commons-lang3.version>3.4</commons-lang3.version>
         <commons-validator.version>1.5.0</commons-validator.version>
-        <jackson.version>2.7.3</jackson.version>
+        <jackson.version>2.8.8.1</jackson.version>
         <json-schema-validator.version>2.2.6</json-schema-validator.version>
         <scala.version>2.11</scala.version>
         <akka.version>2.4.2</akka.version>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
index 80e4e01..aed9a0c 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
@@ -41,7 +41,7 @@ import java.security.cert.X509Certificate;
  */
 @Slf4j
 @Component("MqttSslHandlerProvider")
-@ConditionalOnProperty(prefix = "mqtt.ssl", value = "key-store", havingValue = "", matchIfMissing = false)
+@ConditionalOnProperty(prefix = "mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
 public class MqttSslHandlerProvider {
 
     public static final String TLS = "TLS";
diff --git a/ui/src/app/api/alarm.service.js b/ui/src/app/api/alarm.service.js
new file mode 100644
index 0000000..d9ab4ff
--- /dev/null
+++ b/ui/src/app/api/alarm.service.js
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+export default angular.module('thingsboard.api.alarm', [])
+    .factory('alarmService', AlarmService)
+    .name;
+
+/*@ngInject*/
+function AlarmService($http, $q, $interval, $filter) {
+    var service = {
+        getAlarm: getAlarm,
+        saveAlarm: saveAlarm,
+        ackAlarm: ackAlarm,
+        clearAlarm: clearAlarm,
+        getAlarms: getAlarms,
+        pollAlarms: pollAlarms,
+        cancelPollAlarms: cancelPollAlarms
+    }
+
+    return service;
+
+    function getAlarm(alarmId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/alarm/' + alarmId;
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveAlarm(alarm, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/alarm';
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.post(url, alarm, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function ackAlarm(alarmId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/alarm/' + alarmId + '/ack';
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.post(url, null, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function clearAlarm(alarmId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/alarm/' + alarmId + '/clear';
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.post(url, null, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getAlarms(entityType, entityId, pageLink, alarmStatus, ascOrder, config) {
+        var deferred = $q.defer();
+        var url = '/api/alarm/' + entityType + '/' + entityId + '?limit=' + pageLink.limit;
+
+        if (angular.isDefined(pageLink.startTime)) {
+            url += '&startTime=' + pageLink.startTime;
+        }
+        if (angular.isDefined(pageLink.endTime)) {
+            url += '&endTime=' + pageLink.endTime;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&offset=' + pageLink.idOffset;
+        }
+        if (alarmStatus) {
+            url += '&status=' + alarmStatus;
+        }
+        if (angular.isDefined(ascOrder) && ascOrder != null) {
+            url += '&ascOrder=' + (ascOrder ? 'true' : 'false');
+        }
+
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function fetchAlarms(alarmsQuery, pageLink, deferred, alarmsList) {
+        getAlarms(alarmsQuery.entityType, alarmsQuery.entityId,
+            pageLink, alarmsQuery.alarmStatus, false, {ignoreLoading: true}).then(
+            function success(alarms) {
+                if (!alarmsList) {
+                    alarmsList = [];
+                }
+                alarmsList = alarmsList.concat(alarms.data);
+                if (alarms.hasNext && !alarmsQuery.limit) {
+                    fetchAlarms(alarmsQuery, alarms.nextPageLink, deferred, alarmsList);
+                } else {
+                    alarmsList = $filter('orderBy')(alarmsList, ['-createdTime']);
+                    deferred.resolve(alarmsList);
+                }
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+    }
+
+    function getAlarmsByQuery(alarmsQuery) {
+        var deferred = $q.defer();
+        var time = Date.now();
+        var pageLink;
+        if (alarmsQuery.limit) {
+            pageLink = {
+                limit: alarmsQuery.limit
+            };
+        } else {
+            pageLink = {
+                limit: 100,
+                startTime: time - alarmsQuery.interval
+            };
+        }
+        fetchAlarms(alarmsQuery, pageLink, deferred);
+        return deferred.promise;
+    }
+
+    function onPollAlarms(alarmsQuery) {
+        getAlarmsByQuery(alarmsQuery).then(
+            function success(alarms) {
+                alarmsQuery.onAlarms(alarms);
+            },
+            function fail() {}
+        );
+    }
+
+    function pollAlarms(entityType, entityId, alarmStatus, interval, limit, pollingInterval, onAlarms) {
+        var alarmsQuery = {
+            entityType: entityType,
+            entityId: entityId,
+            alarmStatus: alarmStatus,
+            interval: interval,
+            limit: limit,
+            onAlarms: onAlarms
+        };
+        onPollAlarms(alarmsQuery);
+        return $interval(onPollAlarms, pollingInterval, 0, false, alarmsQuery);
+    }
+
+    function cancelPollAlarms(pollPromise) {
+        if (angular.isDefined(pollPromise)) {
+            $interval.cancel(pollPromise);
+        }
+    }
+
+}
diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js
index f685b1e..2fbb11d 100644
--- a/ui/src/app/api/asset.service.js
+++ b/ui/src/app/api/asset.service.js
@@ -31,7 +31,8 @@ function AssetService($http, $q, customerService, userService) {
         getTenantAssets: getTenantAssets,
         getCustomerAssets: getCustomerAssets,
         findByQuery: findByQuery,
-        fetchAssetsByNameFilter: fetchAssetsByNameFilter
+        fetchAssetsByNameFilter: fetchAssetsByNameFilter,
+        getAssetTypes: getAssetTypes
     }
 
     return service;
@@ -152,7 +153,7 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
-    function getTenantAssets(pageLink, applyCustomersInfo, config) {
+    function getTenantAssets(pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/tenant/assets?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -164,6 +165,9 @@ function AssetService($http, $q, customerService, userService) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomersInfo(response.data.data).then(
@@ -184,7 +188,7 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
-    function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config) {
+    function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/assets?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -196,6 +200,9 @@ function AssetService($http, $q, customerService, userService) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
@@ -258,4 +265,15 @@ function AssetService($http, $q, customerService, userService) {
         return deferred.promise;
     }
 
+    function getAssetTypes() {
+        var deferred = $q.defer();
+        var url = '/api/asset/types';
+        $http.get(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
 }
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index d12d025..b1d1bb6 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -41,12 +41,13 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         deleteDeviceAttributes: deleteDeviceAttributes,
         sendOneWayRpcCommand: sendOneWayRpcCommand,
         sendTwoWayRpcCommand: sendTwoWayRpcCommand,
-        findByQuery: findByQuery
+        findByQuery: findByQuery,
+        getDeviceTypes: getDeviceTypes
     }
 
     return service;
 
-    function getTenantDevices(pageLink, applyCustomersInfo, config) {
+    function getTenantDevices(pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/tenant/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -58,6 +59,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomersInfo(response.data.data).then(
@@ -78,7 +82,7 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         return deferred.promise;
     }
 
-    function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
+    function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config, type) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -90,6 +94,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
+        if (angular.isDefined(type) && type.length) {
+            url += '&type=' + type;
+        }
         $http.get(url, config).then(function success(response) {
             if (applyCustomersInfo) {
                 customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
@@ -286,4 +293,15 @@ function DeviceService($http, $q, attributeService, customerService, types) {
         return deferred.promise;
     }
 
+    function getDeviceTypes() {
+        var deferred = $q.defer();
+        var url = '/api/device/types';
+        $http.get(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
 }
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index 43c537c..891f9d8 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -20,14 +20,13 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
     .name;
 
 /*@ngInject*/
-function EntityService($http, $q, $filter, $translate, userService, deviceService,
+function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
                        assetService, tenantService, customerService,
-                       ruleService, pluginService, entityRelationService, attributeService, types, utils) {
+                       ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
     var service = {
         getEntity: getEntity,
         getEntities: getEntities,
         getEntitiesByNameFilter: getEntitiesByNameFilter,
-        entityName: entityName,
         processEntityAliases: processEntityAliases,
         getEntityKeys: getEntityKeys,
         checkEntityAlias: checkEntityAlias,
@@ -36,7 +35,8 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
         saveRelatedEntity: saveRelatedEntity,
         getRelatedEntity: getRelatedEntity,
         deleteRelatedEntity: deleteRelatedEntity,
-        moveEntity: moveEntity
+        moveEntity: moveEntity,
+        copyEntity: copyEntity
     };
 
     return service;
@@ -62,6 +62,15 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
             case types.entityType.plugin:
                 promise = pluginService.getPlugin(entityId);
                 break;
+            case types.entityType.dashboard:
+                promise = dashboardService.getDashboardInfo(entityId);
+                break;
+            case types.entityType.user:
+                promise = userService.getUser(entityId);
+                break;
+            case types.entityType.alarm:
+                $log.error('Get Alarm Entity is not implemented!');
+                break;
         }
         return promise;
     }
@@ -133,6 +142,15 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
             case types.entityType.plugin:
                 promise = getEntitiesByIdsPromise(pluginService.getPlugin, entityIds);
                 break;
+            case types.entityType.dashboard:
+                promise = getEntitiesByIdsPromise(dashboardService.getDashboardInfo, entityIds);
+                break;
+            case types.entityType.user:
+                promise = getEntitiesByIdsPromise(userService.getUser, entityIds);
+                break;
+            case types.entityType.alarm:
+                $log.error('Get Alarm Entity is not implemented!');
+                break;
         }
         return promise;
     }
@@ -140,34 +158,38 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
     function getEntities(entityType, entityIds, config) {
         var deferred = $q.defer();
         var promise = getEntitiesPromise(entityType, entityIds, config);
-        promise.then(
-            function success(result) {
-                deferred.resolve(result);
-            },
-            function fail() {
-                deferred.reject();
-            }
-        );
+        if (promise) {
+            promise.then(
+                function success(result) {
+                    deferred.resolve(result);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        } else {
+            deferred.reject();
+        }
         return deferred.promise;
     }
 
-    function getEntitiesByPageLinkPromise(entityType, pageLink, config) {
+    function getEntitiesByPageLinkPromise(entityType, pageLink, config, subType) {
         var promise;
         var user = userService.getCurrentUser();
         var customerId = user.customerId;
         switch (entityType) {
             case types.entityType.device:
                 if (user.authority === 'CUSTOMER_USER') {
-                    promise = deviceService.getCustomerDevices(customerId, pageLink, false, config);
+                    promise = deviceService.getCustomerDevices(customerId, pageLink, false, config, subType);
                 } else {
-                    promise = deviceService.getTenantDevices(pageLink, false, config);
+                    promise = deviceService.getTenantDevices(pageLink, false, config, subType);
                 }
                 break;
             case types.entityType.asset:
                 if (user.authority === 'CUSTOMER_USER') {
-                    promise = assetService.getCustomerAssets(customerId, pageLink, false, config);
+                    promise = assetService.getCustomerAssets(customerId, pageLink, false, config, subType);
                 } else {
-                    promise = assetService.getTenantAssets(pageLink, false, config);
+                    promise = assetService.getTenantAssets(pageLink, false, config, subType);
                 }
                 break;
             case types.entityType.tenant:
@@ -182,48 +204,48 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
             case types.entityType.plugin:
                 promise = pluginService.getAllPlugins(pageLink);
                 break;
+            case types.entityType.dashboard:
+                if (user.authority === 'CUSTOMER_USER') {
+                    promise = dashboardService.getCustomerDashboards(customerId, pageLink);
+                } else {
+                    promise = dashboardService.getTenantDashboards(pageLink);
+                }
+                break;
+            case types.entityType.user:
+                $log.error('Get User Entities is not implemented!');
+                break;
+            case types.entityType.alarm:
+                $log.error('Get Alarm Entities is not implemented!');
+                break;
         }
         return promise;
     }
 
-    function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config) {
+    function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config, subType) {
         var deferred = $q.defer();
         var pageLink = {limit: limit, textSearch: entityNameFilter};
-        var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config);
-        promise.then(
-            function success(result) {
-                if (result.data && result.data.length > 0) {
-                    deferred.resolve(result.data);
-                } else {
+        var promise = getEntitiesByPageLinkPromise(entityType, pageLink, config, subType);
+        if (promise) {
+            promise.then(
+                function success(result) {
+                    if (result.data && result.data.length > 0) {
+                        deferred.resolve(result.data);
+                    } else {
+                        deferred.resolve(null);
+                    }
+                },
+                function fail() {
                     deferred.resolve(null);
                 }
-            },
-            function fail() {
-                deferred.resolve(null);
-            }
-        );
-        return deferred.promise;
-    }
-
-    function entityName(entityType, entity) {
-        var name = '';
-        switch (entityType) {
-            case types.entityType.device:
-            case types.entityType.asset:
-            case types.entityType.rule:
-            case types.entityType.plugin:
-                name = entity.name;
-                break;
-            case types.entityType.tenant:
-            case types.entityType.customer:
-                name = entity.title;
-                break;
+            );
+        } else {
+            deferred.resolve(null);
         }
-        return name;
+        return deferred.promise;
     }
 
     function entityToEntityInfo(entityType, entity) {
-        return { name: entityName(entityType, entity), entityType: entityType, id: entity.id.id };
+        return { name: entity.name, entityType: entityType, id: entity.id.id };
     }
 
     function entitiesToEntitiesInfo(entityType, entities) {
@@ -626,6 +648,32 @@ function EntityService($http, $q, $filter, $translate, userService, deviceServic
         return deferred.promise;
     }
 
+    function copyEntity(entity, targetParentId, keys) {
+        var deferred = $q.defer();
+        if (!entity.id && !entity.id.id) {
+            deferred.reject();
+        } else {
+            getRelatedEntity(entity.id, keys).then(
+                function success(relatedEntity) {
+                    delete relatedEntity.id.id;
+                    relatedEntity.name = entity.name;
+                    saveRelatedEntity(relatedEntity, targetParentId, keys).then(
+                        function success(savedEntity) {
+                            deferred.resolve(savedEntity);
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
     function saveEntityPromise(entity) {
         var entityType = entity.id.entityType;
         if (!entity.id.id) {
diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js
index 998607a..875b2fa 100644
--- a/ui/src/app/api/entity-relation.service.js
+++ b/ui/src/app/api/entity-relation.service.js
@@ -25,8 +25,10 @@ function EntityRelationService($http, $q) {
         deleteRelation: deleteRelation,
         deleteRelations: deleteRelations,
         findByFrom: findByFrom,
+        findInfoByFrom: findInfoByFrom,
         findByFromAndType: findByFromAndType,
         findByTo: findByTo,
+        findInfoByTo: findInfoByTo,
         findByToAndType: findByToAndType,
         findByQuery: findByQuery
     }
@@ -84,6 +86,18 @@ function EntityRelationService($http, $q) {
         return deferred.promise;
     }
 
+    function findInfoByFrom(fromId, fromType) {
+        var deferred = $q.defer();
+        var url = '/api/relations/info?fromId=' + fromId;
+        url += '&fromType=' + fromType;
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
     function findByFromAndType(fromId, fromType, relationType) {
         var deferred = $q.defer();
         var url = '/api/relations?fromId=' + fromId;
@@ -109,6 +123,18 @@ function EntityRelationService($http, $q) {
         return deferred.promise;
     }
 
+    function findInfoByTo(toId, toType) {
+        var deferred = $q.defer();
+        var url = '/api/relations/info?toId=' + toId;
+        url += '&toType=' + toType;
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
     function findByToAndType(toId, toType, relationType) {
         var deferred = $q.defer();
         var url = '/api/relations?toId=' + toId;
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
index fef3273..074437d 100644
--- a/ui/src/app/app.config.js
+++ b/ui/src/app/app.config.js
@@ -160,10 +160,6 @@ export default function AppConfig($provide,
             indigoTheme();
         }
 
-        $mdThemingProvider.theme('tb-search-input', 'default')
-            .primaryPalette('tb-primary')
-            .backgroundPalette('tb-primary');
-
         $mdThemingProvider.setDefaultTheme('default');
         //$mdThemingProvider.alwaysWatchTheme(true);
     }
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index f67be33..9ba34b4 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -67,6 +67,7 @@ import thingsboardApiEntityRelation from './api/entity-relation.service';
 import thingsboardApiAsset from './api/asset.service';
 import thingsboardApiAttribute from './api/attribute.service';
 import thingsboardApiEntity from './api/entity.service';
+import thingsboardApiAlarm from './api/alarm.service';
 
 import 'typeface-roboto';
 import 'font-awesome/css/font-awesome.min.css';
@@ -124,6 +125,7 @@ angular.module('thingsboard', [
     thingsboardApiAsset,
     thingsboardApiAttribute,
     thingsboardApiEntity,
+    thingsboardApiAlarm,
     uiRouter])
     .config(AppConfig)
     .factory('globalInterceptor', GlobalInterceptor)
diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js
index d0944ae..1253891 100644
--- a/ui/src/app/asset/asset.controller.js
+++ b/ui/src/app/asset/asset.controller.js
@@ -47,7 +47,8 @@ export function AssetCardController(types) {
 
 
 /*@ngInject*/
-export function AssetController(userService, assetService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams,
+                                $document, $mdDialog, $q, $translate, types) {
 
     var customerId = $stateParams.customerId;
 
@@ -129,8 +130,8 @@ export function AssetController(userService, assetService, customerService, $sta
         }
 
         if (vm.assetsScope === 'tenant') {
-            fetchAssetsFunction = function (pageLink) {
-                return assetService.getTenantAssets(pageLink, true);
+            fetchAssetsFunction = function (pageLink, assetType) {
+                return assetService.getTenantAssets(pageLink, true, null, assetType);
             };
             deleteAssetFunction = function (assetId) {
                 return assetService.deleteAsset(assetId);
@@ -229,8 +230,8 @@ export function AssetController(userService, assetService, customerService, $sta
 
 
         } else if (vm.assetsScope === 'customer' || vm.assetsScope === 'customer_user') {
-            fetchAssetsFunction = function (pageLink) {
-                return assetService.getCustomerAssets(customerId, pageLink, true);
+            fetchAssetsFunction = function (pageLink, assetType) {
+                return assetService.getCustomerAssets(customerId, pageLink, true, null, assetType);
             };
             deleteAssetFunction = function (assetId) {
                 return assetService.unassignAssetFromCustomer(assetId);
@@ -333,6 +334,7 @@ export function AssetController(userService, assetService, customerService, $sta
         var deferred = $q.defer();
         assetService.saveAsset(asset).then(
             function success(savedAsset) {
+                $rootScope.$broadcast('assetSaved');
                 var assets = [ savedAsset ];
                 customerService.applyAssignedCustomersInfo(assets).then(
                     function success(items) {
diff --git a/ui/src/app/asset/asset.directive.js b/ui/src/app/asset/asset.directive.js
index 8c13082..7110e6a 100644
--- a/ui/src/app/asset/asset.directive.js
+++ b/ui/src/app/asset/asset.directive.js
@@ -25,6 +25,7 @@ export default function AssetDirective($compile, $templateCache, toast, $transla
         var template = $templateCache.get(assetFieldsetTemplate);
         element.html(template);
 
+        scope.types = types;
         scope.isAssignedToCustomer = false;
         scope.isPublic = false;
         scope.assignedCustomer = null;
diff --git a/ui/src/app/asset/asset.routes.js b/ui/src/app/asset/asset.routes.js
index c9a312d..732f74c 100644
--- a/ui/src/app/asset/asset.routes.js
+++ b/ui/src/app/asset/asset.routes.js
@@ -20,7 +20,7 @@ import assetsTemplate from './assets.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function AssetRoutes($stateProvider) {
+export default function AssetRoutes($stateProvider, types) {
     $stateProvider
         .state('home.assets', {
             url: '/assets',
@@ -37,6 +37,8 @@ export default function AssetRoutes($stateProvider) {
             data: {
                 assetsType: 'tenant',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.asset,
                 pageTitle: 'asset.assets'
             },
             ncyBreadcrumb: {
@@ -58,6 +60,8 @@ export default function AssetRoutes($stateProvider) {
             data: {
                 assetsType: 'customer',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.asset,
                 pageTitle: 'customer.assets'
             },
             ncyBreadcrumb: {
diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html
index 3c06558..30d0483 100644
--- a/ui/src/app/asset/asset-card.tpl.html
+++ b/ui/src/app/asset/asset-card.tpl.html
@@ -15,5 +15,8 @@
     limitations under the License.
 
 -->
-<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 flex layout="column" style="margin-top: -10px;">
+    <div flex 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/asset-fieldset.tpl.html b/ui/src/app/asset/asset-fieldset.tpl.html
index 8cf0c96..d921b2e 100644
--- a/ui/src/app/asset/asset-fieldset.tpl.html
+++ b/ui/src/app/asset/asset-fieldset.tpl.html
@@ -56,13 +56,13 @@
                 <div translate ng-message="required">asset.name-required</div>
             </div>
         </md-input-container>
-        <md-input-container class="md-block">
-            <label translate>asset.type</label>
-            <input required name="type" ng-model="asset.type">
-            <div ng-messages="theForm.name.$error">
-                <div translate ng-message="required">asset.type-required</div>
-            </div>
-        </md-input-container>
+        <tb-entity-subtype-autocomplete
+                ng-disabled="loading || !isEdit"
+                tb-required="true"
+                the-form="theForm"
+                ng-model="asset.type"
+                entity-type="types.entityType.asset">
+        </tb-entity-subtype-autocomplete>
         <md-input-container class="md-block">
             <label translate>asset.description</label>
             <textarea ng-model="asset.additionalInfo.description" rows="2"></textarea>
diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html
index fb3cf56..11a118f 100644
--- a/ui/src/app/asset/assets.tpl.html
+++ b/ui/src/app/asset/assets.tpl.html
@@ -55,4 +55,10 @@
                             default-event-type="{{vm.types.eventType.alarm.value}}">
             </tb-event-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+            <tb-relation-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.asset}}">
+            </tb-relation-table>
+        </md-tab>
 </tb-grid>
diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js
index 57317b5..78136df 100644
--- a/ui/src/app/common/dashboard-utils.service.js
+++ b/ui/src/app/common/dashboard-utils.service.js
@@ -179,6 +179,7 @@ function DashboardUtils(types, utils, timeService) {
             dashboard.configuration.settings.showEntitiesSelect = true;
             dashboard.configuration.settings.showDashboardTimewindow = true;
             dashboard.configuration.settings.showDashboardExport = true;
+            dashboard.configuration.settings.toolbarAlwaysOpen = false;
         } else {
             if (angular.isUndefined(dashboard.configuration.settings.stateControllerId)) {
                 dashboard.configuration.settings.stateControllerId = 'default';
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index a8d8556..44e59a2 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -59,6 +59,12 @@ export default angular.module('thingsboard.types', [])
                     name: "aggregation.none"
                 }
             },
+            alarmStatus: {
+                activeUnack: "ACTIVE_UNACK",
+                activeAck: "ACTIVE_ACK",
+                clearedUnack: "CLEARED_UNACK",
+                clearedAck: "CLEARED_ACK"
+            },
             position: {
                 top: {
                     value: "top",
@@ -98,7 +104,10 @@ export default angular.module('thingsboard.types', [])
                 rule: "RULE",
                 plugin: "PLUGIN",
                 tenant: "TENANT",
-                customer: "CUSTOMER"
+                customer: "CUSTOMER",
+                user: "USER",
+                dashboard: "DASHBOARD",
+                alarm: "ALARM"
             },
             entitySearchDirection: {
                 from: "FROM",
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index b324cbf..7b4d65e 100644
--- a/ui/src/app/common/utils.service.js
+++ b/ui/src/app/common/utils.service.js
@@ -108,7 +108,8 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
         guid: guid,
         isLocalUrl: isLocalUrl,
         validateDatasources: validateDatasources,
-        createKey: createKey
+        createKey: createKey,
+        entityTypeName: entityTypeName
     }
 
     return service;
@@ -346,4 +347,27 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
         return dataKey;
     }
 
+    function entityTypeName (type) {
+        switch (type) {
+            case types.entityType.device:
+                return 'entity.type-device';
+            case types.entityType.asset:
+                return 'entity.type-asset';
+            case types.entityType.rule:
+                return 'entity.type-rule';
+            case types.entityType.plugin:
+                return 'entity.type-plugin';
+            case types.entityType.tenant:
+                return 'entity.type-tenant';
+            case types.entityType.customer:
+                return 'entity.type-customer';
+            case types.entityType.user:
+                return 'entity.type-user';
+            case types.entityType.dashboard:
+                return 'entity.type-dashboard';
+            case types.entityType.alarm:
+                return 'entity.type-alarm';
+        }
+    }
+
 }
diff --git a/ui/src/app/components/dashboard-autocomplete.tpl.html b/ui/src/app/components/dashboard-autocomplete.tpl.html
index 8b6be20..d1193c2 100644
--- a/ui/src/app/components/dashboard-autocomplete.tpl.html
+++ b/ui/src/app/components/dashboard-autocomplete.tpl.html
@@ -34,7 +34,7 @@
     </md-item-template>
     <md-not-found>
         <div class="tb-not-found">
-            <span translate translate-values='{ dashboard: dashboardSearchText }'>dashboard.no-dashboards-matching</span>
+            <span translate translate-values='{ entity: dashboardSearchText }'>dashboard.no-dashboards-matching</span>
         </div>
     </md-not-found>
     <div ng-messages="theForm.dashboard.$error">
diff --git a/ui/src/app/components/expand-fullscreen.directive.js b/ui/src/app/components/expand-fullscreen.directive.js
index 37491e7..b70fd94 100644
--- a/ui/src/app/components/expand-fullscreen.directive.js
+++ b/ui/src/app/components/expand-fullscreen.directive.js
@@ -24,7 +24,7 @@ export default angular.module('thingsboard.directives.expandFullscreen', [])
 /* eslint-disable angular/angularelement */
 
 /*@ngInject*/
-function ExpandFullscreen($compile, $document) {
+function ExpandFullscreen($compile, $document, $timeout) {
 
     var uniqueId = 1;
     var linker = function (scope, element, attrs) {
@@ -97,10 +97,6 @@ function ExpandFullscreen($compile, $document) {
             scope.expanded = !scope.expanded;
         }
 
-        var expandButton = null;
-        if (attrs.expandButtonId) {
-            expandButton = $('#' + attrs.expandButtonId, element)[0];
-        }
         var buttonSize;
         if (attrs.expandButtonSize) {
             buttonSize = attrs.expandButtonSize;
@@ -115,27 +111,38 @@ function ExpandFullscreen($compile, $document) {
             'options=\'{"easing": "circ-in-out", "duration": 375, "rotation": "none"}\'>' +
             '</ng-md-icon>';
 
-        if (expandButton) {
-            expandButton = angular.element(expandButton);
-            if (scope.hideExpandButton()) {
-                expandButton.remove();
-            } else {
-                expandButton.attr('md-ink-ripple', 'false');
-                expandButton.append(html);
+        if (attrs.expandButtonId) {
+            $timeout(function() {
+               var expandButton = $('#' + attrs.expandButtonId, element)[0];
+                renderExpandButton(expandButton);
+            });
+        } else {
+            renderExpandButton();
+        }
+
+        function renderExpandButton(expandButton) {
+            if (expandButton) {
+                expandButton = angular.element(expandButton);
+                if (scope.hideExpandButton()) {
+                    expandButton.remove();
+                } else {
+                    expandButton.attr('md-ink-ripple', 'false');
+                    expandButton.append(html);
 
-                $compile(expandButton.contents())(scope);
+                    $compile(expandButton.contents())(scope);
 
-                expandButton.on("click", scope.toggleExpand);
-            }
-        } else if (!scope.hideExpandButton()) {
-            var button = angular.element('<md-button class="tb-fullscreen-button-style tb-fullscreen-button-pos md-icon-button" ' +
-                'md-ink-ripple="false" ng-click="toggleExpand($event)">' +
-                html +
-                '</md-button>');
+                    expandButton.on("click", scope.toggleExpand);
+                }
+            } else if (!scope.hideExpandButton()) {
+                var button = angular.element('<md-button class="tb-fullscreen-button-style tb-fullscreen-button-pos md-icon-button" ' +
+                    'md-ink-ripple="false" ng-click="toggleExpand($event)">' +
+                    html +
+                    '</md-button>');
 
-            $compile(button)(scope);
+                $compile(button)(scope);
 
-            element.prepend(button);
+                element.prepend(button);
+            }
         }
     }
 
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index 5664400..296456a 100644
--- a/ui/src/app/components/grid.directive.js
+++ b/ui/src/app/components/grid.directive.js
@@ -197,7 +197,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         },
 
         getLength: function () {
-            if (vm.items.hasNext) {
+            if (vm.items.hasNext && !vm.items.pending) {
                 return vm.items.rowData.length + pageSize;
             } else {
                 return vm.items.rowData.length;
@@ -206,7 +206,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
 
         fetchMoreItems_: function () {
             if (vm.items.hasNext && !vm.items.pending) {
-                var promise = vm.fetchItemsFunc(vm.items.nextPageLink);
+                var promise = vm.fetchItemsFunc(vm.items.nextPageLink, $scope.searchConfig.searchEntitySubtype);
                 if (promise) {
                     vm.items.pending = true;
                     promise.then(
@@ -433,6 +433,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         reload();
     });
 
+    $scope.$on('searchEntitySubtypeUpdated', function () {
+        reload();
+    });
+
     vm.onGridInited(vm);
 
     vm.itemRows.getItemAtIndex(pageSize);
@@ -441,18 +445,16 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
         if (vm.items && vm.items.pending) {
             vm.items.reloadPending = true;
         } else {
-            vm.items = {
-                data: [],
-                rowData: [],
-                nextPageLink: {
-                    limit: pageSize,
-                    textSearch: $scope.searchConfig.searchText
-                },
-                selections: {},
-                selectedCount: 0,
-                hasNext: true,
-                pending: false
+            vm.items.data.length = 0;
+            vm.items.rowData.length = 0;
+            vm.items.nextPageLink = {
+                limit: pageSize,
+                textSearch: $scope.searchConfig.searchText
             };
+            vm.items.selections = {};
+            vm.items.selectedCount = 0;
+            vm.items.hasNext = true;
+            vm.items.pending = false;
             vm.detailsConfig.isDetailsOpen = false;
             vm.items.reloadPending = false;
             vm.itemRows.getItemAtIndex(pageSize);
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
index c29c2e5..24285d8 100644
--- a/ui/src/app/components/grid.tpl.html
+++ b/ui/src/app/components/grid.tpl.html
@@ -24,9 +24,8 @@
         <md-virtual-repeat-container ng-show="vm.hasData()" tb-scope-element="repeatContainer" id="tb-vertical-container" md-top-index="vm.topIndex" flex>
             <div class="md-padding" layout="column">
                 <section layout="row"  md-virtual-repeat="rowItem in vm.itemRows" md-on-demand md-item-size="vm.itemHeight">
-                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}">
-                        <md-card ng-if="rowItem[n]"
-                                 ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
+                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="rowItem[n]">
+                        <md-card ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
                                  class="repeated-item tb-card-item" ng-style="{'height':(vm.itemHeight-16)+'px','cursor':'pointer'}"
                                  ng-click="vm.clickItemFunc($event, rowItem[n])">
                             <section layout="row" layout-wrap>
@@ -43,7 +42,7 @@
                                 </md-card-title>
                             </section>
                             <md-card-content flex>
-                                <tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
+                                <tb-grid-card-content flex grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
                             </md-card-content>
                             <md-card-actions layout="row" layout-align="end end">
                                 <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
@@ -56,6 +55,8 @@
                             </md-card-actions>
                         </md-card>
                     </div>
+                    <div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="!rowItem[n]">
+                    </div>
                 </section>
             </div>
         </md-virtual-repeat-container>
diff --git a/ui/src/app/components/plugin-select.tpl.html b/ui/src/app/components/plugin-select.tpl.html
index 420442e..9a46d7f 100644
--- a/ui/src/app/components/plugin-select.tpl.html
+++ b/ui/src/app/components/plugin-select.tpl.html
@@ -34,7 +34,7 @@
     </md-item-template>
     <md-not-found>
         <div class="tb-not-found">
-            <span translate translate-values='{ plugin: pluginSearchText }'>plugin.no-plugins-matching</span>
+            <span translate translate-values='{ entity: pluginSearchText }'>plugin.no-plugins-matching</span>
         </div>
     </md-not-found>
 </md-autocomplete>
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
index 9221984..6c70a70 100644
--- a/ui/src/app/customer/customers.tpl.html
+++ b/ui/src/app/customer/customers.tpl.html
@@ -55,5 +55,11 @@
 							default-event-type="{{vm.types.eventType.alarm.value}}">
 			</tb-event-table>
 		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+			<tb-relation-table flex
+							   entity-id="vm.grid.operatingItem().id.id"
+							   entity-type="{{vm.types.entityType.customer}}">
+			</tb-relation-table>
+		</md-tab>
 	</md-tabs>
 </tb-grid>
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index c446be4..31892a6 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -63,7 +63,9 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     Object.defineProperty(vm, 'toolbarOpened', {
-        get: function() { return !vm.widgetEditMode && ($scope.forceFullscreen || vm.isToolbarOpened || vm.isEdit || vm.showRightLayoutSwitch()); },
+        get: function() {
+            return !vm.widgetEditMode &&
+                (toolbarAlwaysOpen() || $scope.forceFullscreen || vm.isToolbarOpened || vm.isEdit || vm.showRightLayoutSwitch()); },
         set: function() { }
     });
 
@@ -103,9 +105,11 @@ export default function DashboardController(types, dashboardUtils, widgetService
     }
 
     vm.showCloseToolbar = function() {
-        return !$scope.forceFullscreen && !vm.isEdit && !vm.showRightLayoutSwitch();
+        return !vm.toolbarAlwaysOpen() && !$scope.forceFullscreen && !vm.isEdit && !vm.showRightLayoutSwitch();
     }
 
+    vm.toolbarAlwaysOpen = toolbarAlwaysOpen;
+
     vm.showRightLayoutSwitch = function() {
         return vm.isMobile && vm.layouts.right.show;
     }
@@ -365,7 +369,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
         }
     }
 
-    function openDashboardState(state) {
+    function openDashboardState(state, openRightLayout) {
         var layoutsData = dashboardUtils.getStateLayoutsData(vm.dashboard, state);
         if (layoutsData) {
             vm.dashboardCtx.state = state;
@@ -383,7 +387,7 @@ export default function DashboardController(types, dashboardUtils, widgetService
                     layoutVisibilityChanged = !vm.isMobile;
                 }
             }
-            vm.isRightLayoutOpened = false;
+            vm.isRightLayoutOpened = openRightLayout ? true : false;
             updateLayouts(layoutVisibilityChanged);
         }
 
@@ -738,6 +742,15 @@ export default function DashboardController(types, dashboardUtils, widgetService
         return link;
     }
 
+    function toolbarAlwaysOpen() {
+        if (vm.dashboard && vm.dashboard.configuration.settings &&
+            angular.isDefined(vm.dashboard.configuration.settings.toolbarAlwaysOpen)) {
+            return vm.dashboard.configuration.settings.toolbarAlwaysOpen;
+        } else {
+            return false;
+        }
+    }
+
     function displayTitle() {
         if (vm.dashboard && vm.dashboard.configuration.settings &&
             angular.isDefined(vm.dashboard.configuration.settings.showTitle)) {
diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss
index bc5ec56..ca2bbab 100644
--- a/ui/src/app/dashboard/dashboard.scss
+++ b/ui/src/app/dashboard/dashboard.scss
@@ -68,87 +68,23 @@ section.tb-dashboard-toolbar {
   pointer-events: none;
   &.tb-dashboard-toolbar-opened {
     right: 0px;
-    @include transition(right .3s cubic-bezier(.55,0,.55,.2));
+   // @include transition(right .3s cubic-bezier(.55,0,.55,.2));
   }
   &.tb-dashboard-toolbar-closed {
     right: 18px;
     @include transition(right .3s cubic-bezier(.55,0,.55,.2) .2s);
   }
-  md-fab-toolbar {
-    &.md-is-open {
-      md-fab-trigger {
-        .md-button {
-          &.md-fab {
-            opacity: 1;
-            @include transition(opacity .3s cubic-bezier(.55,0,.55,.2));
-          }
-        }
-      }
-    }
-    md-fab-trigger {
-      .md-button {
-        &.md-fab {
-          line-height: 36px;
-          width: 36px;
-          height: 36px;
-          margin: 4px 0 0 4px;
-          opacity: 0.5;
-          @include transition(opacity .3s cubic-bezier(.55,0,.55,.2) .2s);
-          md-icon {
-            position: absolute;
-            top: 25%;
-            margin: 0;
-            line-height: 18px;
-            height: 18px;
-            width: 18px;
-            min-height: 18px;
-            min-width: 18px;
-          }
-        }
-      }
-    }
-    .md-fab-toolbar-wrapper {
-      height: 50px;
-      md-toolbar {
-        min-height: 46px;
-        height: 46px;
-        md-fab-actions {
-          font-size: 16px;
-          margin-top: 0px;
-          .close-action {
-            margin-right: -18px;
-          }
-          .md-fab-action-item {
-            width: 100%;
-            height: 46px;
-            .tb-dashboard-action-panels {
-              height: 46px;
-              flex-direction: row-reverse;
-              .tb-dashboard-action-panel {
-                height: 46px;
-                flex-direction: row-reverse;
-                div {
-                  height: 46px;
-                }
-                md-select {
-                  pointer-events: all;
-                }
-                tb-states-component {
-                  pointer-events: all;
-                }
-              }
-            }
-          }
-        }
-      }
-    }
-  }
 }
 
 .tb-dashboard-container {
    &.tb-dashboard-toolbar-opened {
-     margin-top: 50px;
-     @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2));
+     &.is-fullscreen {
+       margin-top: 64px;
+     }
+     &:not(.is-fullscreen) {
+       margin-top: 50px;
+       @include transition(margin-top .3s cubic-bezier(.55,0,.55,.2));
+     }
    }
    &.tb-dashboard-toolbar-closed {
      margin-top: 0px;
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 94e016e..8338612 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -19,110 +19,98 @@
             hide-expand-button="vm.widgetEditMode || vm.iframeMode || forceFullscreen" expand-tooltip-direction="bottom">
     <section class="tb-dashboard-toolbar" ng-show="vm.showDashboardToolbar()"
              ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
-        <md-fab-toolbar ng-show="!vm.widgetEditMode" md-open="vm.toolbarOpened"
-                        md-direction="left">
-            <md-fab-trigger class="align-with-text">
-                <md-button aria-label="menu" class="md-fab md-primary" ng-click="vm.openToolbar()">
-                    <md-tooltip ng-show="!vm.toolbarOpened" md-direction="bottom">
-                        {{ 'dashboard.open-toolbar' | translate }}
-                    </md-tooltip>
-                    <md-icon aria-label="dashboard-toolbar" class="material-icons">more_horiz</md-icon>
-                </md-button>
-            </md-fab-trigger>
-            <md-toolbar>
-                <md-fab-actions class="md-toolbar-tools">
-                    <div class="tb-dashboard-action-panels" flex layout="row" layout-align="start center">
-                        <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="start center">
-                            <md-button ng-show="vm.showCloseToolbar()" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
-                                <md-tooltip md-direction="bottom">
-                                    {{ 'dashboard.close-toolbar' | translate }}
-                                </md-tooltip>
-                                <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
-                            </md-button>
-                            <md-button ng-show="vm.showRightLayoutSwitch()" aria-label="switch-layouts" class="md-icon-button" ng-click="vm.toggleLayouts()">
-                                <ng-md-icon icon="{{vm.isRightLayoutOpened ? 'arrow_back' : 'menu'}}" options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
-                                <md-tooltip md-direction="bottom">
-                                    {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}
-                                </md-tooltip>
-                            </md-button>
-                            <md-button id="dashboard-expand-button"
-                                       aria-label="{{ 'fullscreen.fullscreen' | translate }}"
-                                       class="md-icon-button">
-                            </md-button>
-                            <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
-                            </tb-user-menu>
-                            <md-button ng-show="vm.isEdit || vm.displayExport()"
-                                       aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
-                                       ng-click="vm.exportDashboard($event)">
-                                <md-tooltip md-direction="bottom">
-                                    {{ 'dashboard.export' | translate }}
-                                </md-tooltip>
-                                <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
-                            </md-button>
-                            <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
-                                           is-toolbar
-                                           direction="left"
-                                           tooltip-direction="bottom" aggregation
-                                           ng-model="vm.dashboardCtx.dashboardTimewindow">
-                            </tb-timewindow>
-                            <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
-                                                      tooltip-direction="bottom"
-                                                      ng-model="vm.dashboardCtx.aliasesInfo.entityAliases"
-                                                      entity-aliases-info="vm.dashboardCtx.aliasesInfo.entityAliasesInfo">
-                            </tb-aliases-entity-select>
-                            <md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
-                                       ng-click="vm.openEntityAliases($event)">
-                                <md-tooltip md-direction="bottom">
-                                    {{ 'entity.aliases' | translate }}
-                                </md-tooltip>
-                                <md-icon aria-label="{{ 'entity.aliases' | translate }}" class="material-icons">devices_other</md-icon>
-                            </md-button>
-                            <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
-                                       ng-click="vm.openDashboardSettings($event)">
-                                <md-tooltip md-direction="bottom">
-                                    {{ 'dashboard.settings' | translate }}
-                                </md-tooltip>
-                                <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
-                            </md-button>
-                            <tb-dashboard-select   ng-show="!vm.isEdit && !vm.widgetEditMode && vm.displayDashboardsSelect()"
-                                                   ng-model="vm.currentDashboardId"
-                                                   dashboards-scope="{{vm.currentDashboardScope}}"
-                                                   customer-id="vm.currentCustomerId">
-                            </tb-dashboard-select>
-                        </div>
-                        <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="end center">
-                            <div layout="row" layout-align="start center" ng-show="vm.isEdit">
-                                <md-button aria-label="{{ 'dashboard.manage-states' | translate }}" class="md-icon-button"
-                                           ng-click="vm.manageDashboardStates($event)">
-                                    <md-tooltip md-direction="bottom">
-                                        {{ 'dashboard.manage-states' | translate }}
-                                    </md-tooltip>
-                                    <md-icon aria-label="{{ 'dashboard.manage-states' | translate }}" class="material-icons">layers</md-icon>
-                                </md-button>
-                                <md-button aria-label="{{ 'layout.manage' | translate }}" class="md-icon-button"
-                                           ng-click="vm.manageDashboardLayouts($event)">
-                                    <md-tooltip md-direction="bottom">
-                                        {{ 'layout.manage' | translate }}
-                                    </md-tooltip>
-                                    <md-icon aria-label="{{ 'layout.manage' | translate }}" class="material-icons">view_compact</md-icon>
-                                </md-button>
-                            </div>
-                            <div layout="row" layout-align="start center">
-                                <tb-states-component ng-if="vm.isEdit" states-controller-id="'default'"
-                                                     dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
-                                </tb-states-component>
-                                <tb-states-component ng-if="!vm.isEdit" states-controller-id="vm.dashboardConfiguration.settings.stateControllerId"
-                                                     dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
-                                </tb-states-component>
-                            </div>
-                         </div>
+        <tb-dashboard-toolbar ng-show="!vm.widgetEditMode" force-fullscreen="forceFullscreen"
+                              toolbar-opened="vm.toolbarOpened" on-trigger-click="vm.openToolbar()">
+            <div class="tb-dashboard-action-panels" flex layout="row" layout-align="start center">
+                <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="start center">
+                    <md-button ng-show="vm.showCloseToolbar()" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
+                        <md-tooltip md-direction="bottom">
+                            {{ 'dashboard.close-toolbar' | translate }}
+                        </md-tooltip>
+                        <md-icon aria-label="close-toolbar" class="material-icons">arrow_forward</md-icon>
+                    </md-button>
+                    <md-button ng-show="vm.showRightLayoutSwitch()" aria-label="switch-layouts" class="md-icon-button" ng-click="vm.toggleLayouts()">
+                        <ng-md-icon icon="{{vm.isRightLayoutOpened ? 'arrow_back' : 'menu'}}" options='{"easing": "circ-in-out", "duration": 375, "rotation": "none"}'></ng-md-icon>
+                        <md-tooltip md-direction="bottom">
+                            {{ (vm.isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}
+                        </md-tooltip>
+                    </md-button>
+                    <md-button id="dashboard-expand-button"
+                               aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+                               class="md-icon-button">
+                    </md-button>
+                    <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
+                    </tb-user-menu>
+                    <md-button ng-show="vm.isEdit || vm.displayExport()"
+                               aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
+                               ng-click="vm.exportDashboard($event)">
+                        <md-tooltip md-direction="bottom">
+                            {{ 'dashboard.export' | translate }}
+                        </md-tooltip>
+                        <md-icon aria-label="{{ 'action.export' | translate }}" class="material-icons">file_download</md-icon>
+                    </md-button>
+                    <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
+                                   is-toolbar
+                                   direction="left"
+                                   tooltip-direction="bottom" aggregation
+                                   ng-model="vm.dashboardCtx.dashboardTimewindow">
+                    </tb-timewindow>
+                    <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
+                                              tooltip-direction="bottom"
+                                              ng-model="vm.dashboardCtx.aliasesInfo.entityAliases"
+                                              entity-aliases-info="vm.dashboardCtx.aliasesInfo.entityAliasesInfo">
+                    </tb-aliases-entity-select>
+                    <md-button ng-show="vm.isEdit" aria-label="{{ 'entity.aliases' | translate }}" class="md-icon-button"
+                               ng-click="vm.openEntityAliases($event)">
+                        <md-tooltip md-direction="bottom">
+                            {{ 'entity.aliases' | translate }}
+                        </md-tooltip>
+                        <md-icon aria-label="{{ 'entity.aliases' | translate }}" class="material-icons">devices_other</md-icon>
+                    </md-button>
+                    <md-button ng-show="vm.isEdit" aria-label="{{ 'dashboard.settings' | translate }}" class="md-icon-button"
+                               ng-click="vm.openDashboardSettings($event)">
+                        <md-tooltip md-direction="bottom">
+                            {{ 'dashboard.settings' | translate }}
+                        </md-tooltip>
+                        <md-icon aria-label="{{ 'dashboard.settings' | translate }}" class="material-icons">settings</md-icon>
+                    </md-button>
+                    <tb-dashboard-select   ng-show="!vm.isEdit && !vm.widgetEditMode && vm.displayDashboardsSelect()"
+                                           ng-model="vm.currentDashboardId"
+                                           dashboards-scope="{{vm.currentDashboardScope}}"
+                                           customer-id="vm.currentCustomerId">
+                    </tb-dashboard-select>
+                </div>
+                <div class="tb-dashboard-action-panel" flex="50" layout="row" layout-align="end center">
+                    <div layout="row" layout-align="start center" ng-show="vm.isEdit">
+                        <md-button aria-label="{{ 'dashboard.manage-states' | translate }}" class="md-icon-button"
+                                   ng-click="vm.manageDashboardStates($event)">
+                            <md-tooltip md-direction="bottom">
+                                {{ 'dashboard.manage-states' | translate }}
+                            </md-tooltip>
+                            <md-icon aria-label="{{ 'dashboard.manage-states' | translate }}" class="material-icons">layers</md-icon>
+                        </md-button>
+                        <md-button aria-label="{{ 'layout.manage' | translate }}" class="md-icon-button"
+                                   ng-click="vm.manageDashboardLayouts($event)">
+                            <md-tooltip md-direction="bottom">
+                                {{ 'layout.manage' | translate }}
+                            </md-tooltip>
+                            <md-icon aria-label="{{ 'layout.manage' | translate }}" class="material-icons">view_compact</md-icon>
+                        </md-button>
                     </div>
-                </md-fab-actions>
-            </md-toolbar>
-        </md-fab-toolbar>
+                    <div layout="row" layout-align="start center">
+                        <tb-states-component ng-if="vm.isEdit" states-controller-id="'default'"
+                                             dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+                        </tb-states-component>
+                        <tb-states-component ng-if="!vm.isEdit" states-controller-id="vm.dashboardConfiguration.settings.stateControllerId"
+                                             dashboard-ctrl="vm" states="vm.dashboardConfiguration.states">
+                        </tb-states-component>
+                    </div>
+                 </div>
+            </div>
+        </tb-dashboard-toolbar>
     </section>
     <section class="tb-dashboard-container tb-absolute-fill"
-             ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
+             ng-class="{ 'is-fullscreen': forceFullscreen, 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
         <section ng-show="!loading && vm.dashboardConfigurationError()" layout-align="center center"
                  ng-style="{'color': vm.dashboard.configuration.settings.titleColor}"
                  ng-class="{'tb-padded' : !vm.widgetEditMode}"
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index dbe4cbe..ac98709 100644
--- a/ui/src/app/dashboard/dashboard-settings.controller.js
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -57,6 +57,10 @@ export default function DashboardSettingsController($scope, $mdDialog, statesCon
         if (angular.isUndefined(vm.settings.showDashboardExport)) {
             vm.settings.showDashboardExport = true;
         }
+
+        if (angular.isUndefined(vm.settings.toolbarAlwaysOpen)) {
+            vm.settings.toolbarAlwaysOpen = false;
+        }
     }
 
     if (vm.gridSettings) {
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
index 88fc66d..f3ff704 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -41,6 +41,9 @@
                             </md-select>
                         </md-input-container>
                         <div layout="row" layout-align="start center">
+                            <md-checkbox flex aria-label="{{ 'dashboard.toolbar-always-open' | translate }}"
+                                         ng-model="vm.settings.toolbarAlwaysOpen">{{ 'dashboard.toolbar-always-open' | translate }}
+                            </md-checkbox>
                             <md-checkbox flex aria-label="{{ 'dashboard.display-title' | translate }}"
                                          ng-model="vm.settings.showTitle">{{ 'dashboard.display-title' | translate }}
                             </md-checkbox>
diff --git a/ui/src/app/dashboard/dashboard-toolbar.directive.js b/ui/src/app/dashboard/dashboard-toolbar.directive.js
new file mode 100644
index 0000000..63a34f8
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-toolbar.directive.js
@@ -0,0 +1,88 @@
+/*
+ * 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 './dashboard-toolbar.scss';
+
+import 'javascript-detect-element-resize/detect-element-resize';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import dashboardToolbarTemplate from './dashboard-toolbar.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function DashboardToolbar() {
+    return {
+        restrict: "E",
+        scope: true,
+        transclude: true,
+        bindToController: {
+            toolbarOpened: '=',
+            forceFullscreen: '=',
+            onTriggerClick: '&'
+        },
+        controller: DashboardToolbarController,
+        controllerAs: 'vm',
+        templateUrl: dashboardToolbarTemplate
+    };
+}
+
+/* eslint-disable angular/angularelement */
+
+
+/*@ngInject*/
+function DashboardToolbarController($scope, $element, $timeout, mdFabToolbarAnimation) {
+
+    let vm = this;
+
+    vm.mdFabToolbarElement = angular.element($element[0].querySelector('md-fab-toolbar'));
+
+    $timeout(function() {
+        vm.mdFabBackgroundElement = angular.element(vm.mdFabToolbarElement[0].querySelector('.md-fab-toolbar-background'));
+        vm.mdFabTriggerElement = angular.element(vm.mdFabToolbarElement[0].querySelector('md-fab-trigger button'));
+    });
+
+    addResizeListener(vm.mdFabToolbarElement[0], triggerFabResize); // eslint-disable-line no-undef
+
+    $scope.$on("$destroy", function () {
+        removeResizeListener(vm.mdFabToolbarElement[0], triggerFabResize); // eslint-disable-line no-undef
+    });
+
+    function triggerFabResize() {
+        var ctrl = vm.mdFabToolbarElement.controller('mdFabToolbar');
+        if (ctrl.isOpen) {
+            if (!vm.mdFabBackgroundElement[0].offsetWidth) {
+                mdFabToolbarAnimation.addClass(vm.mdFabToolbarElement, 'md-is-open', function () {
+                });
+            } else {
+                var color = window.getComputedStyle(vm.mdFabTriggerElement[0]).getPropertyValue('background-color'); //eslint-disable-line
+
+                var width = vm.mdFabToolbarElement[0].offsetWidth;
+                var scale = 2 * (width / vm.mdFabTriggerElement[0].offsetWidth);
+                vm.mdFabBackgroundElement[0].style.backgroundColor = color;
+                vm.mdFabBackgroundElement[0].style.borderRadius = width + 'px';
+
+                var transform = vm.mdFabBackgroundElement[0].style.transform;
+                var targetTransform = 'scale(' + scale + ')';
+                if (!transform || !angular.equals(transform, targetTransform)) {
+                    vm.mdFabBackgroundElement[0].style.transform = targetTransform;
+                }
+            }
+        }
+    }
+}
+
diff --git a/ui/src/app/dashboard/dashboard-toolbar.scss b/ui/src/app/dashboard/dashboard-toolbar.scss
new file mode 100644
index 0000000..af4889e
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-toolbar.scss
@@ -0,0 +1,114 @@
+/**
+ * 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 "~compass-sass-mixins/lib/compass";
+@import '../../scss/constants';
+
+tb-dashboard-toolbar {
+  md-fab-toolbar {
+    &.md-is-open {
+      md-fab-trigger {
+        .md-button {
+          &.md-fab {
+            opacity: 1;
+            @include transition(opacity .3s cubic-bezier(.55,0,.55,.2));
+            .md-fab-toolbar-background {
+                background-color: $primary-default !important;
+            }
+          }
+        }
+      }
+    }
+    md-fab-trigger {
+      .md-button {
+        &.md-fab {
+          line-height: 36px;
+          width: 36px;
+          height: 36px;
+          margin: 4px 0 0 4px;
+          opacity: 0.5;
+          @include transition(opacity .3s cubic-bezier(.55,0,.55,.2) .2s);
+          md-icon {
+            position: absolute;
+            top: 25%;
+            margin: 0;
+            line-height: 18px;
+            height: 18px;
+            width: 18px;
+            min-height: 18px;
+            min-width: 18px;
+          }
+        }
+      }
+    }
+    &.is-fullscreen {
+      &.md-is-open {
+        md-fab-trigger {
+          .md-button {
+            &.md-fab {
+              .md-fab-toolbar-background {
+                  transition-delay: 0ms !important;
+                  transition-duration: 0ms !important;
+              }
+            }
+          }
+        }
+      }
+      .md-fab-toolbar-wrapper {
+        height: 64px;
+        md-toolbar {
+          min-height: 64px;
+          height: 64px;
+        }
+      }
+    }
+    .md-fab-toolbar-wrapper {
+      height: 50px;
+      md-toolbar {
+        min-height: 50px;
+        height: 50px;
+        md-fab-actions {
+          font-size: 16px;
+          margin-top: 0px;
+          .close-action {
+            margin-right: -18px;
+          }
+          .md-fab-action-item {
+            width: 100%;
+            height: 46px;
+            .tb-dashboard-action-panels {
+              height: 46px;
+              flex-direction: row-reverse;
+              .tb-dashboard-action-panel {
+                height: 46px;
+                flex-direction: row-reverse;
+                div {
+                  height: 46px;
+                }
+                md-select {
+                  pointer-events: all;
+                }
+                tb-states-component {
+                  pointer-events: all;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/dashboard-toolbar.tpl.html b/ui/src/app/dashboard/dashboard-toolbar.tpl.html
new file mode 100644
index 0000000..46192ff
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-toolbar.tpl.html
@@ -0,0 +1,35 @@
+<!--
+
+    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-fab-toolbar md-open="vm.toolbarOpened"
+                md-direction="left"
+                ng-class="{'is-fullscreen': vm.forceFullscreen, 'md-whiteframe-z1': vm.forceFullscreen}">
+    <md-fab-trigger class="align-with-text">
+        <md-button aria-label="menu" class="md-fab md-primary" ng-click="vm.onTriggerClick()">
+            <md-tooltip ng-show="!vm.toolbarOpened" md-direction="bottom">
+                {{ 'dashboard.open-toolbar' | translate }}
+            </md-tooltip>
+            <md-icon aria-label="dashboard-toolbar" class="material-icons">more_horiz</md-icon>
+        </md-button>
+    </md-fab-trigger>
+    <md-toolbar>
+        <md-fab-actions class="md-toolbar-tools">
+            <div ng-transclude></div>
+        </md-fab-actions>
+    </md-toolbar>
+</md-fab-toolbar>
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index 73a97a1..d940f09 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -45,6 +45,7 @@ import AddDashboardsToCustomerController from './add-dashboards-to-customer.cont
 import AddWidgetController from './add-widget.controller';
 import DashboardDirective from './dashboard.directive';
 import EditWidgetDirective from './edit-widget.directive';
+import DashboardToolbar from './dashboard-toolbar.directive';
 
 export default angular.module('thingsboard.dashboard', [
     uiRouter,
@@ -78,4 +79,5 @@ export default angular.module('thingsboard.dashboard', [
     .controller('AddWidgetController', AddWidgetController)
     .directive('tbDashboardDetails', DashboardDirective)
     .directive('tbEditWidget', EditWidgetDirective)
+    .directive('tbDashboardToolbar', DashboardToolbar)
     .name;
diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js
index 782f59e..76ea9b8 100644
--- a/ui/src/app/dashboard/states/default-state-controller.js
+++ b/ui/src/app/dashboard/states/default-state-controller.js
@@ -26,12 +26,13 @@ export default function DefaultStateController($scope, $location, $state, $state
     vm.navigatePrevState = navigatePrevState;
     vm.getStateId = getStateId;
     vm.getStateParams = getStateParams;
+    vm.getStateParamsByStateId = getStateParamsByStateId;
 
     vm.getStateName = getStateName;
 
     vm.displayStateSelection = displayStateSelection;
 
-    function openState(id, params) {
+    function openState(id, params, openRightLayout) {
         if (vm.states && vm.states[id]) {
             if (!params) {
                 params = {};
@@ -42,11 +43,11 @@ export default function DefaultStateController($scope, $location, $state, $state
             }
             //append new state
             vm.stateObject[0] = newState;
-            gotoState(vm.stateObject[0].id, true);
+            gotoState(vm.stateObject[0].id, true, openRightLayout);
         }
     }
 
-    function updateState(id, params) {
+    function updateState(id, params, openRightLayout) {
         if (vm.states && vm.states[id]) {
             if (!params) {
                 params = {};
@@ -57,7 +58,7 @@ export default function DefaultStateController($scope, $location, $state, $state
             }
             //replace with new state
             vm.stateObject[0] = newState;
-            gotoState(vm.stateObject[0].id, true);
+            gotoState(vm.stateObject[0].id, true, openRightLayout);
         }
     }
 
@@ -76,6 +77,24 @@ export default function DefaultStateController($scope, $location, $state, $state
         return vm.stateObject[vm.stateObject.length-1].params;
     }
 
+    function getStateParamsByStateId(stateId) {
+        var stateObj = getStateObjById(stateId);
+        if (stateObj) {
+            return stateObj.params;
+        } else {
+            return null;
+        }
+    }
+
+    function getStateObjById(id) {
+        for (var i=0; i < vm.stateObject.length; i++) {
+            if (vm.stateObject[i].id === id) {
+                return vm.stateObject[i];
+            }
+        }
+        return null;
+    }
+
     function getStateName(id, state) {
         var result = '';
         var translationId = types.translate.dashboardStatePrefix + id;
@@ -161,9 +180,9 @@ export default function DefaultStateController($scope, $location, $state, $state
         }, true);
     }
 
-    function gotoState(stateId, update) {
+    function gotoState(stateId, update, openRightLayout) {
         if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
-            vm.dashboardCtrl.openDashboardState(stateId);
+            vm.dashboardCtrl.openDashboardState(stateId, openRightLayout);
             if (update) {
                 updateLocation();
             }
diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js
index 3eaf453..51cf67d 100644
--- a/ui/src/app/dashboard/states/entity-state-controller.js
+++ b/ui/src/app/dashboard/states/entity-state-controller.js
@@ -28,12 +28,13 @@ export default function EntityStateController($scope, $location, $state, $stateP
     vm.navigatePrevState = navigatePrevState;
     vm.getStateId = getStateId;
     vm.getStateParams = getStateParams;
+    vm.getStateParamsByStateId = getStateParamsByStateId;
 
     vm.getStateName = getStateName;
 
     vm.selectedStateIndex = -1;
 
-    function openState(id, params) {
+    function openState(id, params, openRightLayout) {
         if (vm.states && vm.states[id]) {
             resolveEntity(params).then(
                 function success(entityName) {
@@ -45,13 +46,13 @@ export default function EntityStateController($scope, $location, $state, $stateP
                     //append new state
                     vm.stateObject.push(newState);
                     vm.selectedStateIndex = vm.stateObject.length-1;
-                    gotoState(vm.stateObject[vm.stateObject.length-1].id, true);
+                    gotoState(vm.stateObject[vm.stateObject.length-1].id, true, openRightLayout);
                 }
             );
         }
     }
 
-    function updateState(id, params) {
+    function updateState(id, params, openRightLayout) {
         if (vm.states && vm.states[id]) {
             resolveEntity(params).then(
                 function success(entityName) {
@@ -62,7 +63,7 @@ export default function EntityStateController($scope, $location, $state, $stateP
                     }
                     //replace with new state
                     vm.stateObject[vm.stateObject.length - 1] = newState;
-                    gotoState(vm.stateObject[vm.stateObject.length - 1].id, true);
+                    gotoState(vm.stateObject[vm.stateObject.length - 1].id, true, openRightLayout);
                 }
             );
         }
@@ -84,6 +85,24 @@ export default function EntityStateController($scope, $location, $state, $stateP
         return vm.stateObject[vm.stateObject.length-1].params;
     }
 
+    function getStateParamsByStateId(stateId) {
+        var stateObj = getStateObjById(stateId);
+        if (stateObj) {
+            return stateObj.params;
+        } else {
+            return null;
+        }
+    }
+
+    function getStateObjById(id) {
+        for (var i=0; i < vm.stateObject.length; i++) {
+            if (vm.stateObject[i].id === id) {
+                return vm.stateObject[i];
+            }
+        }
+        return null;
+    }
+
     function getStateName(index) {
         var result = '';
         if (vm.stateObject[index]) {
@@ -109,7 +128,7 @@ export default function EntityStateController($scope, $location, $state, $stateP
         if (params && params.entityId && params.entityId.id && params.entityId.entityType) {
             entityService.getEntity(params.entityId.entityType, params.entityId.id, {ignoreLoading: true, ignoreErrors: true}).then(
                 function success(entity) {
-                    var entityName = entityService.entityName(params.entityId.entityType, entity);
+                    var entityName = entity.name;
                     deferred.resolve(entityName);
                 },
                 function fail() {
@@ -223,9 +242,9 @@ export default function EntityStateController($scope, $location, $state, $stateP
         });
     }
 
-    function gotoState(stateId, update) {
+    function gotoState(stateId, update, openRightLayout) {
         if (vm.dashboardCtrl.dashboardCtx.state != stateId) {
-            vm.dashboardCtrl.openDashboardState(stateId);
+            vm.dashboardCtrl.openDashboardState(stateId, openRightLayout);
             if (update) {
                 updateLocation();
             }
diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
index 151c05a..9f86b9b 100644
--- a/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
+++ b/ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
@@ -58,7 +58,7 @@
                                         {{ 'dashboard.search-states' | translate }}
                                     </md-tooltip>
                                 </md-button>
-                                <md-input-container md-theme="tb-search-input" flex>
+                                <md-input-container flex>
                                     <label>&nbsp;</label>
                                     <input ng-model="vm.query.search" placeholder="{{ 'dashboard.search-states' | translate }}"/>
                                 </md-input-container>
diff --git a/ui/src/app/dashboard/states/states-component.directive.js b/ui/src/app/dashboard/states/states-component.directive.js
index fb5e77c..538c3e9 100644
--- a/ui/src/app/dashboard/states/states-component.directive.js
+++ b/ui/src/app/dashboard/states/states-component.directive.js
@@ -29,15 +29,15 @@ export default function StatesComponent($compile, $templateCache, $controller, s
 
             var stateController = scope.dashboardCtrl.dashboardCtx.stateController;
 
-            stateController.openState = function(id, params) {
+            stateController.openState = function(id, params, openRightLayout) {
                 if (scope.statesController) {
-                    scope.statesController.openState(id, params);
+                    scope.statesController.openState(id, params, openRightLayout);
                 }
             }
 
-            stateController.updateState = function(id, params) {
+            stateController.updateState = function(id, params, openRightLayout) {
                 if (scope.statesController) {
-                    scope.statesController.updateState(id, params);
+                    scope.statesController.updateState(id, params, openRightLayout);
                 }
             }
 
@@ -62,6 +62,14 @@ export default function StatesComponent($compile, $templateCache, $controller, s
                     return {};
                 }
             }
+
+            stateController.getStateParamsByStateId = function(id) {
+                if (scope.statesController) {
+                    return scope.statesController.getStateParamsByStateId(id);
+                } else {
+                    return null;
+                }
+            }
         }
 
         scope.$on('$destroy', function callOnDestroyHook() {
diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js
index 55b7083..3e8c9ac 100644
--- a/ui/src/app/device/device.controller.js
+++ b/ui/src/app/device/device.controller.js
@@ -48,7 +48,8 @@ export function DeviceCardController(types) {
 
 
 /*@ngInject*/
-export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams,
+                                 $document, $mdDialog, $q, $translate, types) {
 
     var customerId = $stateParams.customerId;
 
@@ -131,8 +132,8 @@ export function DeviceController(userService, deviceService, customerService, $s
         }
 
         if (vm.devicesScope === 'tenant') {
-            fetchDevicesFunction = function (pageLink) {
-                return deviceService.getTenantDevices(pageLink, true);
+            fetchDevicesFunction = function (pageLink, deviceType) {
+                return deviceService.getTenantDevices(pageLink, true, null, deviceType);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.deleteDevice(deviceId);
@@ -242,8 +243,8 @@ export function DeviceController(userService, deviceService, customerService, $s
 
 
         } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
-            fetchDevicesFunction = function (pageLink) {
-                return deviceService.getCustomerDevices(customerId, pageLink, true);
+            fetchDevicesFunction = function (pageLink, deviceType) {
+                return deviceService.getCustomerDevices(customerId, pageLink, true, null, deviceType);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.unassignDeviceFromCustomer(deviceId);
@@ -368,6 +369,7 @@ export function DeviceController(userService, deviceService, customerService, $s
         var deferred = $q.defer();
         deviceService.saveDevice(device).then(
             function success(savedDevice) {
+                $rootScope.$broadcast('deviceSaved');
                 var devices = [ savedDevice ];
                 customerService.applyAssignedCustomersInfo(devices).then(
                     function success(items) {
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index eb50a15..eda4fb2 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -25,6 +25,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
         var template = $templateCache.get(deviceFieldsetTemplate);
         element.html(template);
 
+        scope.types = types;
         scope.isAssignedToCustomer = false;
         scope.isPublic = false;
         scope.assignedCustomer = null;
diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js
index c1f5b27..b1ca7ed 100644
--- a/ui/src/app/device/device.routes.js
+++ b/ui/src/app/device/device.routes.js
@@ -20,7 +20,7 @@ import devicesTemplate from './devices.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DeviceRoutes($stateProvider) {
+export default function DeviceRoutes($stateProvider, types) {
     $stateProvider
         .state('home.devices', {
             url: '/devices',
@@ -37,6 +37,8 @@ export default function DeviceRoutes($stateProvider) {
             data: {
                 devicesType: 'tenant',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.device,
                 pageTitle: 'device.devices'
             },
             ncyBreadcrumb: {
@@ -58,6 +60,8 @@ export default function DeviceRoutes($stateProvider) {
             data: {
                 devicesType: 'customer',
                 searchEnabled: true,
+                searchByEntitySubtype: true,
+                searchEntityType: types.entityType.device,
                 pageTitle: 'customer.devices'
             },
             ncyBreadcrumb: {
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index bb84af7..58f4c85 100644
--- a/ui/src/app/device/device-card.tpl.html
+++ b/ui/src/app/device/device-card.tpl.html
@@ -15,5 +15,8 @@
     limitations under the License.
 
 -->
-<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 flex layout="column" style="margin-top: -10px;">
+    <div flex 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/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index fd6c9d1..767e47a 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -66,6 +66,13 @@
 	      		<div translate ng-message="required">device.name-required</div>
 	    	</div>				
 		</md-input-container>
+        <tb-entity-subtype-autocomplete
+                ng-disabled="loading || !isEdit"
+                tb-required="true"
+                the-form="theForm"
+                ng-model="device.type"
+                entity-type="types.entityType.device">
+        </tb-entity-subtype-autocomplete>
         <md-input-container class="md-block">
             <md-checkbox ng-disabled="loading || !isEdit" flex aria-label="{{ 'device.is-gateway' | translate }}"
                          ng-model="device.additionalInfo.gateway">{{ 'device.is-gateway' | translate }}
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 2f90db8..3ff6c1c 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -56,4 +56,10 @@
                             default-event-type="{{vm.types.eventType.alarm.value}}">
             </tb-event-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+            <tb-relation-table flex
+                               entity-id="vm.grid.operatingItem().id.id"
+                               entity-type="{{vm.types.entityType.device}}">
+            </tb-relation-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 bf3b8cd..1afd1bb 100644
--- a/ui/src/app/entity/attribute/attribute-table.tpl.html
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -63,7 +63,7 @@
                         {{ 'action.search' | translate }}
                     </md-tooltip>
                 </md-button>
-                <md-input-container md-theme="tb-search-input" flex>
+                <md-input-container flex>
                     <label>&nbsp;</label>
                     <input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
                 </md-input-container>
@@ -128,9 +128,9 @@
             <table md-table md-row-select multiple="" ng-model="selectedAttributes" md-progress="attributesDeferred.promise">
                 <thead md-head md-order="query.order" md-on-reorder="onReorder">
                     <tr md-row>
-                        <th md-column md-order-by="lastUpdateTs"><span>Last update time</span></th>
-                        <th md-column md-order-by="key"><span>Key</span></th>
-                        <th md-column>Value</th>
+                        <th md-column md-order-by="lastUpdateTs"><span translate>attribute.last-update-time</span></th>
+                        <th md-column md-order-by="key"><span translate>attribute.key</span></th>
+                        <th md-column><span translate>attribute.value</span></th>
                     </tr>
                 </thead>
                 <tbody md-body>
diff --git a/ui/src/app/entity/entity-aliases.controller.js b/ui/src/app/entity/entity-aliases.controller.js
index 4eb1737..f1e2e38 100644
--- a/ui/src/app/entity/entity-aliases.controller.js
+++ b/ui/src/app/entity/entity-aliases.controller.js
@@ -110,7 +110,7 @@ export default function EntityAliasesController(utils, entityService, toast, $sc
                 entityAlias.changed = false;
             }
             if (!entityAlias.changed && entity && entityAlias.entityType) {
-                entityAlias.alias = entityService.entityName(entityAlias.entityType, entity);
+                entityAlias.alias = entity.name;
             }
         }
     }
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
new file mode 100644
index 0000000..350bde9
--- /dev/null
+++ b/ui/src/app/entity/entity-autocomplete.directive.js
@@ -0,0 +1,171 @@
+/*
+ * 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 './entity-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entityAutocompleteTemplate from './entity-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntityAutocomplete($compile, $templateCache, $q, $filter, entityService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entityAutocompleteTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.entity = null;
+        scope.entitySearchText = '';
+
+        scope.fetchEntities = function(searchText) {
+            var deferred = $q.defer();
+            entityService.getEntitiesByNameFilter(scope.entityType, searchText, 50, null, scope.entitySubtype).then(function success(result) {
+                if (result) {
+                    deferred.resolve(result);
+                } else {
+                    deferred.resolve([]);
+                }
+            }, function fail() {
+                deferred.reject();
+            });
+            return deferred.promise;
+        }
+
+        scope.entitySearchTextChanged = function() {
+        }
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                ngModelCtrl.$setViewValue(scope.entity ? scope.entity.id.id : null);
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                entityService.getEntity(scope.entityType, ngModelCtrl.$viewValue).then(
+                    function success(entity) {
+                        scope.entity = entity;
+                    },
+                    function fail() {
+                        scope.entity = null;
+                    }
+                );
+            } else {
+                scope.entity = null;
+            }
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('entitySubtype', function () {
+            if (scope.entity && scope.entity.type != scope.entitySubtype) {
+                scope.entity = null;
+                scope.updateView();
+            }
+        });
+
+        scope.$watch('entity', function () {
+            scope.updateView();
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+
+        function load() {
+            switch (scope.entityType) {
+                case types.entityType.asset:
+                    scope.selectEntityText = 'asset.select-asset';
+                    scope.entityText = 'asset.asset';
+                    scope.noEntitiesMatchingText = 'asset.no-assets-matching';
+                    scope.entityRequiredText = 'asset.asset-required'
+                    break;
+                case types.entityType.device:
+                    scope.selectEntityText = 'device.select-device';
+                    scope.entityText = 'device.device';
+                    scope.noEntitiesMatchingText = 'device.no-devices-matching';
+                    scope.entityRequiredText = 'device.device-required'
+                    break;
+                case types.entityType.rule:
+                    scope.selectEntityText = 'rule.select-rule';
+                    scope.entityText = 'rule.rule';
+                    scope.noEntitiesMatchingText = 'rule.no-rules-matching';
+                    scope.entityRequiredText = 'rule.rule-required'
+                    break;
+                case types.entityType.plugin:
+                    scope.selectEntityText = 'plugin.select-plugin';
+                    scope.entityText = 'plugin.plugin';
+                    scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
+                    scope.entityRequiredText = 'plugin.plugin-required'
+                    break;
+                case types.entityType.tenant:
+                    scope.selectEntityText = 'tenant.select-tenant';
+                    scope.entityText = 'tenant.tenant';
+                    scope.noEntitiesMatchingText = 'tenant.no-tenants-matching';
+                    scope.entityRequiredText = 'tenant.tenant-required'
+                    break;
+                case types.entityType.customer:
+                    scope.selectEntityText = 'customer.select-customer';
+                    scope.entityText = 'customer.customer';
+                    scope.noEntitiesMatchingText = 'customer.no-customers-matching';
+                    scope.entityRequiredText = 'customer.customer-required'
+                    break;
+                case types.entityType.user:
+                    scope.selectEntityText = 'user.select-user';
+                    scope.entityText = 'user.user';
+                    scope.noEntitiesMatchingText = 'user.no-users-matching';
+                    scope.entityRequiredText = 'user.user-required'
+                    break;
+                case types.entityType.dashboard:
+                    scope.selectEntityText = 'dashboard.select-dashboard';
+                    scope.entityText = 'dashboard.dashboard';
+                    scope.noEntitiesMatchingText = 'dashboard.no-dashboards-matching';
+                    scope.entityRequiredText = 'dashboard.dashboard-required'
+                    break;
+                case types.entityType.alarm:
+                    scope.selectEntityText = 'alarm.select-alarm';
+                    scope.entityText = 'alarm.alarm';
+                    scope.noEntitiesMatchingText = 'alarm.no-alarms-matching';
+                    scope.entityRequiredText = 'alarm.alarm-required'
+                    break;
+            }
+            if (scope.entity && scope.entity.id.entityType != scope.entityType) {
+                scope.entity = null;
+                scope.updateView();
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled',
+            entityType: '=',
+            entitySubtype: '=?'
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-autocomplete.tpl.html b/ui/src/app/entity/entity-autocomplete.tpl.html
new file mode 100644
index 0000000..ca6f37b
--- /dev/null
+++ b/ui/src/app/entity/entity-autocomplete.tpl.html
@@ -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.
+
+-->
+<md-autocomplete ng-required="tbRequired"
+                 ng-disabled="disabled"
+                 md-no-cache="true"
+                 md-input-name="entity"
+                 ng-model="entity"
+                 md-selected-item="entity"
+                 md-search-text="entitySearchText"
+                 md-search-text-change="entitySearchTextChanged()"
+                 md-items="item in fetchEntities(entitySearchText)"
+                 md-item-text="item.name"
+                 md-min-length="0"
+                 md-floating-label="{{ entityText | translate }}"
+                 md-select-on-match="true"
+                 md-menu-class="tb-entity-autocomplete">
+    <md-item-template>
+        <div class="tb-entity-item">
+            <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
+        </div>
+    </md-item-template>
+    <md-not-found>
+        <div class="tb-not-found">
+            <span translate translate-values='{ entity: entitySearchText }'>{{ noEntitiesMatchingText }}</span>
+        </div>
+    </md-not-found>
+    <div ng-messages="theForm.entity.$error">
+        <div translate ng-message="required">{{ entityRequiredText }}</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js
index 777dbb8..b1d92c6 100644
--- a/ui/src/app/entity/entity-filter.directive.js
+++ b/ui/src/app/entity/entity-filter.directive.js
@@ -32,14 +32,6 @@ export default function EntityFilterDirective($compile, $templateCache, $q, enti
 
         scope.ngModelCtrl = ngModelCtrl;
 
-        scope.itemName = function(item) {
-            if (item) {
-                return entityService.entityName(scope.entityType, item);
-            } else {
-                return '';
-            }
-        }
-
         scope.fetchEntities = function(searchText, limit) {
             var deferred = $q.defer();
             entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit).then(function success(result) {
diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html
index 508e4ba..7e49ba9 100644
--- a/ui/src/app/entity/entity-filter.tpl.html
+++ b/ui/src/app/entity/entity-filter.tpl.html
@@ -33,7 +33,7 @@
                         md-min-length="0"
                         placeholder="{{ 'entity.entity-list' | translate }}">
                         <md-item-template>
-                            <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{itemName(item)}}</span>
+                            <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{item.name}}</span>
                         </md-item-template>
                         <md-not-found>
                             <span translate translate-values='{ entity: entitySearchText }'>entity.no-entities-matching</span>
@@ -41,7 +41,7 @@
                 </md-autocomplete>
                 <md-chip-template>
                     <span>
-                      <strong>{{itemName($chip)}}</strong>
+                      <strong>{{$chip.name}}</strong>
                     </span>
                 </md-chip-template>
             </md-chips>
diff --git a/ui/src/app/entity/entity-select.directive.js b/ui/src/app/entity/entity-select.directive.js
new file mode 100644
index 0000000..705c6b8
--- /dev/null
+++ b/ui/src/app/entity/entity-select.directive.js
@@ -0,0 +1,86 @@
+/*
+ * 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 './entity-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySelectTemplate from './entity-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySelect($compile, $templateCache) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entitySelectTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.model = {};
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                var value = ngModelCtrl.$viewValue;
+                if (scope.model && scope.model.entityType && scope.model.entityId) {
+                    if (!value) {
+                        value = {};
+                    }
+                    value.entityType = scope.model.entityType;
+                    value.id = scope.model.entityId;
+                    ngModelCtrl.$setViewValue(value);
+                } else {
+                    ngModelCtrl.$setViewValue(null);
+                }
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                scope.model.entityType = value.entityType;
+                scope.model.entityId = value.id;
+            } else {
+                scope.model.entityType = null;
+                scope.model.entityId = null;
+            }
+        }
+
+        scope.$watch('model.entityType', function () {
+            scope.updateView();
+        });
+
+        scope.$watch('model.entityId', function () {
+            scope.updateView();
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled'
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-select.tpl.html b/ui/src/app/entity/entity-select.tpl.html
new file mode 100644
index 0000000..13e17e7
--- /dev/null
+++ b/ui/src/app/entity/entity-select.tpl.html
@@ -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.
+
+-->
+<div layout='row' class="tb-entity-select">
+    <tb-entity-type-select style="min-width: 100px;"
+                           the-form="theForm"
+                           ng-disabled="disabled"
+                           tb-required="tbRequired"
+                           ng-model="model.entityType">
+    </tb-entity-type-select>
+    <tb-entity-autocomplete flex ng-if="model.entityType"
+                            the-form="theForm"
+                            ng-disabled="disabled"
+                            tb-required="tbRequired"
+                            entity-type="model.entityType"
+                            ng-model="model.entityId">
+    </tb-entity-autocomplete>
+</div>
\ No newline at end of file
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.directive.js b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
new file mode 100644
index 0000000..98110b0
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-autocomplete.directive.js
@@ -0,0 +1,141 @@
+/*
+ * 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 './entity-subtype-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySubtypeAutocompleteTemplate from './entity-subtype-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entitySubtypeAutocompleteTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.subType = null;
+        scope.subTypeSearchText = '';
+        scope.entitySubtypes = null;
+
+        scope.fetchSubTypes = function(searchText) {
+            var deferred = $q.defer();
+            loadSubTypes().then(
+                function success(subTypes) {
+                    var result = $filter('filter')(subTypes, {'$': searchText});
+                    if (result && result.length) {
+                        deferred.resolve(result);
+                    } else {
+                        deferred.resolve([searchText]);
+                    }
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+            return deferred.promise;
+        }
+
+        scope.subTypeSearchTextChanged = function() {
+        }
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                ngModelCtrl.$setViewValue(scope.subType);
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            scope.subType = ngModelCtrl.$viewValue;
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('subType', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+        function loadSubTypes() {
+            var deferred = $q.defer();
+            if (!scope.entitySubtypes) {
+                var entitySubtypesPromise;
+                if (scope.entityType == types.entityType.asset) {
+                    entitySubtypesPromise = assetService.getAssetTypes();
+                } else if (scope.entityType == types.entityType.device) {
+                    entitySubtypesPromise = deviceService.getDeviceTypes();
+                }
+                if (entitySubtypesPromise) {
+                    entitySubtypesPromise.then(
+                        function success(types) {
+                            scope.entitySubtypes = [];
+                            types.forEach(function (type) {
+                                scope.entitySubtypes.push(type.type);
+                            });
+                            deferred.resolve(scope.entitySubtypes);
+                        },
+                        function fail() {
+                            deferred.reject();
+                        }
+                    );
+                } else {
+                    deferred.reject();
+                }
+            } else {
+                deferred.resolve(scope.entitySubtypes);
+            }
+            return deferred.promise;
+        }
+
+        function load() {
+            if (scope.entityType == types.entityType.asset) {
+                scope.selectEntitySubtypeText = 'asset.select-asset-type';
+                scope.entitySubtypeText = 'asset.asset-type';
+                scope.entitySubtypeRequiredText = 'asset.asset-type-required';
+            } else if (scope.entityType == types.entityType.device) {
+                scope.selectEntitySubtypeText = 'device.select-device-type';
+                scope.entitySubtypeText = 'device.device-type';
+                scope.entitySubtypeRequiredText = 'device.device-type-required';
+                scope.$on('deviceSaved', function() {
+                    scope.entitySubtypes = null;
+                });
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled',
+            entityType: "="
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-subtype-autocomplete.tpl.html b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
new file mode 100644
index 0000000..ce220ee
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-autocomplete.tpl.html
@@ -0,0 +1,41 @@
+<!--
+
+    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-autocomplete ng-required="tbRequired"
+                 ng-disabled="disabled"
+                 md-no-cache="true"
+                 md-input-name="subType"
+                 ng-model="subType"
+                 md-selected-item="subType"
+                 md-search-text="subTypeSearchText"
+                 md-search-text-change="subTypeSearchTextChanged()"
+                 md-items="item in fetchSubTypes(subTypeSearchText)"
+                 md-item-text="item"
+                 md-min-length="0"
+                 placeholder="{{ selectEntitySubtypeText | translate }}"
+                 md-floating-label="{{ entitySubtypeText | translate }}"
+                 md-select-on-match="true"
+                 md-menu-class="tb-entity-subtype-autocomplete">
+    <md-item-template>
+        <div class="tb-entity-subtype-item">
+            <span md-highlight-text="subTypeSearchText" md-highlight-flags="^i">{{item}}</span>
+        </div>
+    </md-item-template>
+    <div ng-messages="theForm.subType.$error">
+        <div translate ng-message="required">{{ entitySubtypeRequiredText }}</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/entity/entity-subtype-select.directive.js b/ui/src/app/entity/entity-subtype-select.directive.js
new file mode 100644
index 0000000..36d9729
--- /dev/null
+++ b/ui/src/app/entity/entity-subtype-select.directive.js
@@ -0,0 +1,138 @@
+/*
+ * 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 './entity-subtype-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitySubtypeSelectTemplate from './entity-subtype-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntitySubtypeSelect($compile, $templateCache, $translate, assetService, deviceService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entitySubtypeSelectTemplate);
+        element.html(template);
+
+        if (angular.isDefined(attrs.hideLabel)) {
+            scope.showLabel = false;
+        } else {
+            scope.showLabel = true;
+        }
+
+        scope.ngModelCtrl = ngModelCtrl;
+
+        scope.entitySubtypes = [];
+
+        scope.subTypeName = function(subType) {
+            if (subType && subType.length) {
+                if (scope.typeTranslatePrefix) {
+                    return $translate.instant(scope.typeTranslatePrefix + '.' + subType);
+                } else {
+                    return subType;
+                }
+            } else {
+                return $translate.instant('entity.all-subtypes');
+            }
+        }
+
+        scope.$watch('entityType', function () {
+            load();
+        });
+
+        scope.$watch('entitySubtype', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.updateView = function () {
+            ngModelCtrl.$setViewValue(scope.entitySubtype);
+        };
+
+        ngModelCtrl.$render = function () {
+            scope.entitySubtype = ngModelCtrl.$viewValue;
+        };
+
+        function loadSubTypes() {
+            scope.entitySubtypes = [];
+            var entitySubtypesPromise;
+            if (scope.entityType == types.entityType.asset) {
+                entitySubtypesPromise = assetService.getAssetTypes();
+            } else if (scope.entityType == types.entityType.device) {
+                entitySubtypesPromise = deviceService.getDeviceTypes();
+            }
+            if (entitySubtypesPromise) {
+                entitySubtypesPromise.then(
+                    function success(types) {
+                        scope.entitySubtypes.push('');
+                        types.forEach(function(type) {
+                            scope.entitySubtypes.push(type.type);
+                        });
+                        if (scope.entitySubtypes.indexOf(scope.entitySubtype) == -1) {
+                            scope.entitySubtype = '';
+                        }
+                    },
+                    function fail() {}
+                );
+            }
+
+        }
+
+        function load() {
+            if (scope.entityType == types.entityType.asset) {
+                scope.entitySubtypeTitle = 'asset.asset-type';
+                scope.entitySubtypeRequiredText = 'asset.asset-type-required';
+            } else if (scope.entityType == types.entityType.device) {
+                scope.entitySubtypeTitle = 'device.device-type';
+                scope.entitySubtypeRequiredText = 'device.device-type-required';
+            }
+            scope.entitySubtypes.length = 0;
+            if (scope.entitySubtypesList && scope.entitySubtypesList.length) {
+                scope.entitySubtypesList.forEach(function(subType) {
+                    scope.entitySubtypes.push(subType);
+                });
+            } else {
+                loadSubTypes();
+                if (scope.entityType == types.entityType.asset) {
+                    scope.$on('assetSaved', function() {
+                        loadSubTypes();
+                    });
+                } else if (scope.entityType == types.entityType.device) {
+                    scope.$on('deviceSaved', function() {
+                        loadSubTypes();
+                    });
+                }
+            }
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            entityType: "=",
+            entitySubtypesList: "=?",
+            typeTranslatePrefix: "@?"
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js
index 6a5a045..1dc6741 100644
--- a/ui/src/app/entity/entity-type-select.directive.js
+++ b/ui/src/app/entity/entity-type-select.directive.js
@@ -23,12 +23,14 @@ import entityTypeSelectTemplate from './entity-type-select.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function EntityTypeSelect($compile, $templateCache, userService, types) {
+export default function EntityTypeSelect($compile, $templateCache, utils, userService, types) {
 
     var linker = function (scope, element, attrs, ngModelCtrl) {
         var template = $templateCache.get(entityTypeSelectTemplate);
         element.html(template);
 
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+
         if (angular.isDefined(attrs.hideLabel)) {
             scope.showLabel = false;
         } else {
@@ -51,10 +53,12 @@ export default function EntityTypeSelect($compile, $templateCache, userService, 
                 scope.entityTypes.customer = types.entityType.customer;
                 scope.entityTypes.rule = types.entityType.rule;
                 scope.entityTypes.plugin = types.entityType.plugin;
+                scope.entityTypes.dashboard = types.entityType.dashboard;
                 break;
             case 'CUSTOMER_USER':
                 scope.entityTypes.device = types.entityType.device;
                 scope.entityTypes.asset = types.entityType.asset;
+                scope.entityTypes.dashboard = types.entityType.dashboard;
                 break;
         }
 
@@ -67,20 +71,7 @@ export default function EntityTypeSelect($compile, $templateCache, userService, 
         }
 
         scope.typeName = function(type) {
-            switch (type) {
-                case types.entityType.device:
-                    return 'entity.type-device';
-                case types.entityType.asset:
-                    return 'entity.type-asset';
-                case types.entityType.rule:
-                    return 'entity.type-rule';
-                case types.entityType.plugin:
-                    return 'entity.type-plugin';
-                case types.entityType.tenant:
-                    return 'entity.type-tenant';
-                case types.entityType.customer:
-                    return 'entity.type-customer';
-            }
+            return utils.entityTypeName(type);
         }
 
         scope.updateValidity = function () {
@@ -114,6 +105,9 @@ export default function EntityTypeSelect($compile, $templateCache, userService, 
         require: "^ngModel",
         link: linker,
         scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled',
             allowedEntityTypes: "=?"
         }
     };
diff --git a/ui/src/app/entity/entity-type-select.tpl.html b/ui/src/app/entity/entity-type-select.tpl.html
index e31cbf5..86d0eeb 100644
--- a/ui/src/app/entity/entity-type-select.tpl.html
+++ b/ui/src/app/entity/entity-type-select.tpl.html
@@ -17,9 +17,13 @@
 -->
 <md-input-container>
     <label ng-if="showLabel">{{ 'entity.type' | translate }}</label>
-    <md-select ng-model="entityType" class="tb-entity-type-select" aria-label="{{ 'entity.type' | translate }}">
+    <md-select ng-required="tbRequired" ng-disabled="disabled" name="entityType"
+               ng-model="entityType" class="tb-entity-type-select" aria-label="{{ 'entity.type' | translate }}">
         <md-option ng-repeat="type in entityTypes" ng-value="type">
             {{typeName(type) | translate}}
         </md-option>
     </md-select>
-</md-input-container>
\ No newline at end of file
+    <div ng-messages="theForm.entityType.$error">
+        <div ng-message="required" translate>entity.type-required</div>
+    </div>
+</md-input-container>
diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js
index 07c0862..e8cc437 100644
--- a/ui/src/app/entity/index.js
+++ b/ui/src/app/entity/index.js
@@ -16,12 +16,18 @@
 
 import EntityAliasesController from './entity-aliases.controller';
 import EntityTypeSelectDirective from './entity-type-select.directive';
+import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
+import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
+import EntityAutocompleteDirective from './entity-autocomplete.directive';
+import EntitySelectDirective from './entity-select.directive';
 import EntityFilterDirective from './entity-filter.directive';
 import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';
 import AliasesEntitySelectDirective from './aliases-entity-select.directive';
 import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
 import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
 import AttributeTableDirective from './attribute/attribute-table.directive';
+import RelationTableDirective from './relation/relation-table.directive';
+import RelationTypeAutocompleteDirective from './relation/relation-type-autocomplete.directive';
 
 export default angular.module('thingsboard.entity', [])
     .controller('EntityAliasesController', EntityAliasesController)
@@ -29,7 +35,13 @@ export default angular.module('thingsboard.entity', [])
     .controller('AddAttributeDialogController', AddAttributeDialogController)
     .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
     .directive('tbEntityTypeSelect', EntityTypeSelectDirective)
+    .directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
+    .directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
+    .directive('tbEntityAutocomplete', EntityAutocompleteDirective)
+    .directive('tbEntitySelect', EntitySelectDirective)
     .directive('tbEntityFilter', EntityFilterDirective)
     .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
     .directive('tbAttributeTable', AttributeTableDirective)
+    .directive('tbRelationTable', RelationTableDirective)
+    .directive('tbRelationTypeAutocomplete', RelationTypeAutocompleteDirective)
     .name;
diff --git a/ui/src/app/entity/relation/relation-table.directive.js b/ui/src/app/entity/relation/relation-table.directive.js
new file mode 100644
index 0000000..729ff36
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-table.directive.js
@@ -0,0 +1,251 @@
+/*
+ * 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 './relation-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import relationTableTemplate from './relation-table.tpl.html';
+import addRelationTemplate from './add-relation-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import AddRelationController from './add-relation-dialog.controller';
+
+/*@ngInject*/
+export default function RelationTable() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            entityId: '=',
+            entityType: '@'
+        },
+        controller: RelationTableController,
+        controllerAs: 'vm',
+        templateUrl: relationTableTemplate
+    };
+}
+
+/*@ngInject*/
+function RelationTableController($scope, $q, $mdDialog, $document, $translate, $filter, utils, types, entityRelationService) {
+
+    let vm = this;
+
+    vm.types = types;
+
+    vm.direction = vm.types.entitySearchDirection.from;
+
+    vm.relations = [];
+    vm.relationsCount = 0;
+    vm.allRelations = [];
+    vm.selectedRelations = [];
+
+    vm.query = {
+        order: 'type',
+        limit: 5,
+        page: 1,
+        search: null
+    };
+
+    vm.enterFilterMode = enterFilterMode;
+    vm.exitFilterMode = exitFilterMode;
+    vm.onReorder = onReorder;
+    vm.onPaginate = onPaginate;
+    vm.addRelation = addRelation;
+    vm.deleteRelation = deleteRelation;
+    vm.deleteRelations = deleteRelations;
+    vm.reloadRelations = reloadRelations;
+    vm.updateRelations = updateRelations;
+
+    $scope.$watch("vm.entityId", function(newVal, prevVal) {
+        if (newVal && !angular.equals(newVal, prevVal)) {
+            reloadRelations();
+        }
+    });
+
+    $scope.$watch("vm.direction", function(newVal, prevVal) {
+        if (newVal && !angular.equals(newVal, prevVal)) {
+            reloadRelations();
+        }
+    });
+
+    $scope.$watch("vm.query.search", function(newVal, prevVal) {
+        if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+            updateRelations();
+        }
+    });
+
+    function enterFilterMode () {
+        vm.query.search = '';
+    }
+
+    function exitFilterMode () {
+        vm.query.search = null;
+        updateRelations();
+    }
+
+    function onReorder () {
+        updateRelations();
+    }
+
+    function onPaginate () {
+        updateRelations();
+    }
+
+    function addRelation($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var entityId = {
+            id: vm.entityId,
+            entityType: vm.entityType
+        };
+        $mdDialog.show({
+            controller: AddRelationController,
+            controllerAs: 'vm',
+            templateUrl: addRelationTemplate,
+            parent: angular.element($document[0].body),
+            locals: { direction: vm.direction, entityId: entityId },
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function () {
+            reloadRelations();
+        }, function () {
+        });
+    }
+
+    function deleteRelation($event, relation) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if (relation) {
+            var title;
+            var content;
+            if (vm.direction == vm.types.entitySearchDirection.from) {
+                title = $translate.instant('relation.delete-to-relation-title', {entityName: relation.toName});
+                content = $translate.instant('relation.delete-to-relation-text', {entityName: relation.toName});
+            } else {
+                title = $translate.instant('relation.delete-from-relation-title', {entityName: relation.fromName});
+                content = $translate.instant('relation.delete-from-relation-text', {entityName: relation.fromName});
+            }
+
+            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 () {
+                entityRelationService.deleteRelation(
+                    relation.from.id,
+                    relation.from.entityType,
+                    relation.type,
+                    relation.to.id,
+                    relation.to.entityType).then(
+                    function success() {
+                        reloadRelations();
+                    }
+                );
+            });
+        }
+    }
+
+    function deleteRelations($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        if (vm.selectedRelations && vm.selectedRelations.length > 0) {
+            var title;
+            var content;
+            if (vm.direction == vm.types.entitySearchDirection.from) {
+                title = $translate.instant('relation.delete-to-relations-title', {count: vm.selectedRelations.length}, 'messageformat');
+                content = $translate.instant('relation.delete-to-relations-text');
+            } else {
+                title = $translate.instant('relation.delete-from-relations-title', {count: vm.selectedRelations.length}, 'messageformat');
+                content = $translate.instant('relation.delete-from-relations-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 tasks = [];
+                for (var i=0;i<vm.selectedRelations.length;i++) {
+                    var relation = vm.selectedRelations[i];
+                    tasks.push( entityRelationService.deleteRelation(
+                        relation.from.id,
+                        relation.from.entityType,
+                        relation.type,
+                        relation.to.id,
+                        relation.to.entityType));
+                }
+                $q.all(tasks).then(function () {
+                    reloadRelations();
+                });
+
+            });
+        }
+    }
+
+    function reloadRelations () {
+        vm.allRelations.length = 0;
+        vm.relations.length = 0;
+        vm.relationsPromise;
+        if (vm.direction == vm.types.entitySearchDirection.from) {
+            vm.relationsPromise = entityRelationService.findInfoByFrom(vm.entityId, vm.entityType);
+        } else {
+            vm.relationsPromise = entityRelationService.findInfoByTo(vm.entityId, vm.entityType);
+        }
+        vm.relationsPromise.then(
+            function success(allRelations) {
+                allRelations.forEach(function(relation) {
+                    if (vm.direction == vm.types.entitySearchDirection.from) {
+                        relation.toEntityTypeName = $translate.instant(utils.entityTypeName(relation.to.entityType));
+                    } else {
+                        relation.fromEntityTypeName = $translate.instant(utils.entityTypeName(relation.from.entityType));
+                    }
+                });
+                vm.allRelations = allRelations;
+                vm.selectedRelations = [];
+                vm.updateRelations();
+                vm.relationsPromise = null;
+            },
+            function fail() {
+                vm.allRelations = [];
+                vm.selectedRelations = [];
+                vm.updateRelations();
+                vm.relationsPromise = null;
+            }
+        )
+    }
+
+    function updateRelations () {
+        vm.selectedRelations = [];
+        var result = $filter('orderBy')(vm.allRelations, vm.query.order);
+        if (vm.query.search != null) {
+            result = $filter('filter')(result, {$: vm.query.search});
+        }
+        vm.relationsCount = result.length;
+        var startIndex = vm.query.limit * (vm.query.page - 1);
+        vm.relations = result.slice(startIndex, startIndex + vm.query.limit);
+    }
+
+}
diff --git a/ui/src/app/entity/relation/relation-table.tpl.html b/ui/src/app/entity/relation/relation-table.tpl.html
new file mode 100644
index 0000000..c93755b
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-table.tpl.html
@@ -0,0 +1,131 @@
+<!--
+
+    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-relation-table tb-data-table" layout="column">
+    <section layout="row">
+        <md-input-container class="md-block" style="width: 200px;">
+            <label translate>relation.direction</label>
+            <md-select ng-model="vm.direction" ng-disabled="loading">
+                <md-option ng-repeat="direction in vm.types.entitySearchDirection" ng-value="direction">
+                    {{ ('relation.search-direction.' + direction) | translate}}
+                </md-option>
+            </md-select>
+        </md-input-container>
+    </section>
+    <div layout="column" class="md-whiteframe-z1">
+        <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedRelations.length
+                                                                 && vm.query.search === null">
+            <div class="md-toolbar-tools">
+                <span>{{(vm.direction == vm.types.entitySearchDirection.from ?
+                    'relation.from-relations' : 'relation.to-relations') | translate}}</span>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.addRelation($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.reloadRelations()">
+                    <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.selectedRelations.length
+                                                                 && vm.query.search != null">
+            <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="top">
+                        {{ 'action.close' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedRelations.length">
+            <div class="md-toolbar-tools">
+                <span translate
+                      translate-values="{count: vm.selectedRelations.length}"
+                      translate-interpolation="messageformat">relation.selected-relations</span>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.deleteRelations($event)">
+                    <md-icon>delete</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.delete' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-table-container>
+            <table md-table md-row-select multiple="" ng-model="vm.selectedRelations" md-progress="vm.relationsDeferred.promise">
+                <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+                <tr md-row>
+                    <th md-column md-order-by="type"><span translate>relation.type</span></th>
+                    <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.from"
+                        md-order-by="toEntityTypeName"><span translate>relation.to-entity-type</span></th>
+                    <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.to"
+                        md-order-by="fromEntityTypeName"><span translate>relation.from-entity-type</span></th>
+                    <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.from"
+                        md-order-by="toName"><span translate>relation.to-entity-name</span></th>
+                    <th md-column ng-if="vm.direction == vm.types.entitySearchDirection.to"
+                        md-order-by="fromName"><span translate>relation.from-entity-name</span></th>
+                    <th md-column><span>&nbsp</span></th>
+                </tr>
+                </thead>
+                <tbody md-body>
+                <tr md-row md-select="relation" md-select-id="relation" md-auto-select ng-repeat="relation in vm.relations">
+                    <td md-cell>{{ relation.type }}</td>
+                    <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.from">{{ relation.toEntityTypeName  }}</td>
+                    <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.to">{{ relation.fromEntityTypeName  }}</td>
+                    <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.from">{{ relation.toName }}</td>
+                    <td md-cell ng-if="vm.direction == vm.types.entitySearchDirection.to">{{ relation.fromName }}</td>
+                    <td md-cell class="tb-action-cell">
+                        <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteRelation($event, relation)">
+                            <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'relation.delete' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </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.relationsCount}}"
+                             md-on-paginate="onPaginate" md-page-select>
+        </md-table-pagination>
+    </div>
+</md-content>
diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.directive.js b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js
new file mode 100644
index 0000000..a7e5ea4
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-type-autocomplete.directive.js
@@ -0,0 +1,86 @@
+/*
+ * 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 './relation-type-autocomplete.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import relationTypeAutocompleteTemplate from './relation-type-autocomplete.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RelationTypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(relationTypeAutocompleteTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+        scope.relationType = null;
+        scope.relationTypeSearchText = '';
+        scope.relationTypes = [];
+        for (var type in types.entityRelationType) {
+            scope.relationTypes.push(types.entityRelationType[type]);
+        }
+
+        scope.fetchRelationTypes = function(searchText) {
+            var deferred = $q.defer();
+            var result = $filter('filter')(scope.relationTypes, {'$': searchText});
+            if (result && result.length) {
+                deferred.resolve(result);
+            } else {
+                deferred.resolve([searchText]);
+            }
+            return deferred.promise;
+        }
+
+        scope.relationTypeSearchTextChanged = function() {
+        }
+
+        scope.updateView = function () {
+            if (!scope.disabled) {
+                ngModelCtrl.$setViewValue(scope.relationType);
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            scope.relationType = ngModelCtrl.$viewValue;
+        }
+
+        scope.$watch('relationType', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.$watch('disabled', function () {
+            scope.updateView();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            theForm: '=?',
+            tbRequired: '=?',
+            disabled:'=ngDisabled'
+        }
+    };
+}
diff --git a/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
new file mode 100644
index 0000000..f71c134
--- /dev/null
+++ b/ui/src/app/entity/relation/relation-type-autocomplete.tpl.html
@@ -0,0 +1,40 @@
+<!--
+
+    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-autocomplete ng-required="tbRequired"
+                 ng-disabled="disabled"
+                 md-no-cache="true"
+                 md-input-name="relationType"
+                 ng-model="relationType"
+                 md-selected-item="relationType"
+                 md-search-text="relationTypeSearchText"
+                 md-search-text-change="relationTypeSearchTextChanged()"
+                 md-items="item in fetchRelationTypes(relationTypeSearchText)"
+                 md-item-text="item"
+                 md-min-length="0"
+                 md-floating-label="{{ 'relation.relation-type' | translate }}"
+                 md-select-on-match="true"
+                 md-menu-class="tb-relation-type-autocomplete">
+    <md-item-template>
+        <div class="tb-relation-type-item">
+            <span md-highlight-text="relationTypeSearchText" md-highlight-flags="^i">{{item}}</span>
+        </div>
+    </md-item-template>
+    <div ng-messages="theForm.relationType.$error">
+        <div translate ng-message="required">relation.relation-type-required</div>
+    </div>
+</md-autocomplete>
diff --git a/ui/src/app/layout/home.controller.js b/ui/src/app/layout/home.controller.js
index 4979501..5091da8 100644
--- a/ui/src/app/layout/home.controller.js
+++ b/ui/src/app/layout/home.controller.js
@@ -25,7 +25,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
 /* eslint-disable angular/angularelement */
 
 /*@ngInject*/
-export default function HomeController(loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
+export default function HomeController(types, loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
                                        $window, $log, $mdMedia, $animate, $timeout) {
 
     var siteSideNav = $('.tb-site-sidenav', $element);
@@ -38,8 +38,11 @@ export default function HomeController(loginService, userService, deviceService,
     if (angular.isUndefined($rootScope.searchConfig)) {
         $rootScope.searchConfig = {
             searchEnabled: false,
+            searchByEntitySubtype: false,
+            searchEntityType: null,
             showSearch: false,
-            searchText: ""
+            searchText: "",
+            searchEntitySubtype: ""
         };
     }
 
@@ -47,6 +50,7 @@ export default function HomeController(loginService, userService, deviceService,
     vm.isLockSidenav = false;
 
     vm.displaySearchMode = displaySearchMode;
+    vm.displayEntitySubtypeSearch = displayEntitySubtypeSearch;
     vm.openSidenav = openSidenav;
     vm.goBack = goBack;
     vm.searchTextUpdated = searchTextUpdated;
@@ -54,25 +58,35 @@ export default function HomeController(loginService, userService, deviceService,
     vm.toggleFullscreen = toggleFullscreen;
 
     $scope.$on('$stateChangeSuccess', function (evt, to, toParams, from) {
+        watchEntitySubtype(false);
         if (angular.isDefined(to.data.searchEnabled)) {
             $scope.searchConfig.searchEnabled = to.data.searchEnabled;
+            $scope.searchConfig.searchByEntitySubtype = to.data.searchByEntitySubtype;
+            $scope.searchConfig.searchEntityType = to.data.searchEntityType;
             if ($scope.searchConfig.searchEnabled === false || to.name !== from.name) {
                 $scope.searchConfig.showSearch = false;
                 $scope.searchConfig.searchText = "";
+                $scope.searchConfig.searchEntitySubtype = "";
             }
         } else {
             $scope.searchConfig.searchEnabled = false;
+            $scope.searchConfig.searchByEntitySubtype = false;
+            $scope.searchConfig.searchEntityType = null;
             $scope.searchConfig.showSearch = false;
             $scope.searchConfig.searchText = "";
+            $scope.searchConfig.searchEntitySubtype = "";
         }
+        watchEntitySubtype($scope.searchConfig.searchByEntitySubtype);
     });
 
-    if ($mdMedia('gt-sm')) {
+    vm.isGtSm = $mdMedia('gt-sm');
+    if (vm.isGtSm) {
         vm.isLockSidenav = true;
         $animate.enabled(siteSideNav, false);
     }
 
     $scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) {
+        vm.isGtSm = isGtSm;
         vm.isLockSidenav = isGtSm;
         vm.isShowSidenav = isGtSm;
         if (!isGtSm) {
@@ -84,11 +98,28 @@ export default function HomeController(loginService, userService, deviceService,
         }
     });
 
+    function watchEntitySubtype(enableWatch) {
+        if ($scope.entitySubtypeWatch) {
+            $scope.entitySubtypeWatch();
+        }
+        if (enableWatch) {
+            $scope.entitySubtypeWatch = $scope.$watch('searchConfig.searchEntitySubtype', function (newVal, prevVal) {
+                if (!angular.equals(newVal, prevVal)) {
+                    $scope.$broadcast('searchEntitySubtypeUpdated');
+                }
+            });
+        }
+    }
+
     function displaySearchMode() {
         return $scope.searchConfig.searchEnabled &&
             $scope.searchConfig.showSearch;
     }
 
+    function displayEntitySubtypeSearch() {
+        return $scope.searchConfig.searchByEntitySubtype && vm.isGtSm;
+    }
+
     function toggleFullscreen() {
         if (Fullscreen.isEnabled()) {
             Fullscreen.cancel();
diff --git a/ui/src/app/layout/home.scss b/ui/src/app/layout/home.scss
index edcdbc8..a809cf6 100644
--- a/ui/src/app/layout/home.scss
+++ b/ui/src/app/layout/home.scss
@@ -70,3 +70,11 @@ md-icon.tb-logo-title {
   z-index: 2;
   white-space: nowrap;
 }
+
+.tb-entity-subtype-search {
+  margin-top: 15px;
+}
+
+.tb-entity-search {
+  margin-top: 34px;
+}
diff --git a/ui/src/app/layout/home.tpl.html b/ui/src/app/layout/home.tpl.html
index bfb37eb..8e642eb 100644
--- a/ui/src/app/layout/home.tpl.html
+++ b/ui/src/app/layout/home.tpl.html
@@ -39,7 +39,7 @@
   </md-sidenav>
 
   <div flex layout="column" tabIndex="-1" role="main">
-    <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
+    <md-toolbar class="md-whiteframe-z1 tb-primary-toolbar">
     	<div layout="row" flex class="md-toolbar-tools">
 		      <md-button id="main" hide-gt-sm ng-show="!forceFullscreen"
 		      		class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
@@ -55,10 +55,18 @@
 			  <div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools">
 				  <span ng-cloak ncy-breadcrumb></span>
 			  </div>
-			  <md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>
-	              <label>&nbsp;</label>
-	              <input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
-	          </md-input-container>		      
+			  <div layout="row" ng-show="vm.displaySearchMode()" md-theme="tb-dark" flex>
+				  <div class="tb-entity-subtype-search" layout="row" layout-align="start center" ng-if="vm.displayEntitySubtypeSearch()">
+					  <tb-entity-subtype-select
+							  entity-type="searchConfig.searchEntityType"
+							  ng-model="searchConfig.searchEntitySubtype">
+					  </tb-entity-subtype-select>
+				  </div>
+				  <md-input-container ng-class="{'tb-entity-search': vm.displayEntitySubtypeSearch()}" flex>
+					  <label>&nbsp;</label>
+					  <input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
+				  </md-input-container>
+			  </div>
 		      <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}" ng-show="searchConfig.searchEnabled" ng-click="searchConfig.showSearch = !searchConfig.showSearch">
 		      	  <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
           	  </md-button>
diff --git a/ui/src/app/layout/user-menu.directive.js b/ui/src/app/layout/user-menu.directive.js
index 6d09e67..53e8c45 100644
--- a/ui/src/app/layout/user-menu.directive.js
+++ b/ui/src/app/layout/user-menu.directive.js
@@ -48,16 +48,10 @@ function UserMenuController($scope, userService, $translate, $state) {
     var dashboardUser = userService.getCurrentUser();
 
     vm.authorityName = authorityName;
-    vm.displaySearchMode = displaySearchMode;
     vm.logout = logout;
     vm.openProfile = openProfile;
     vm.userDisplayName = userDisplayName;
 
-    function displaySearchMode() {
-        return $scope.searchConfig.searchEnabled &&
-            $scope.searchConfig.showSearch;
-    }
-
     function authorityName() {
         var name = "user.anonymous";
         if (dashboardUser) {
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 6e6308d..fcde13c 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -106,6 +106,12 @@ export default angular.module('thingsboard.locale', [])
                     "enable-tls": "Enable TLS",
                     "send-test-mail": "Send test mail"
                 },
+                "alarm": {
+                    "alarm": "Alarm",
+                    "select-alarm": "Select alarm",
+                    "no-alarms-matching": "No alarms matching '{{entity}}' were found.",
+                    "alarm-required": "Alarm is required"
+                },
                 "asset": {
                     "asset": "Asset",
                     "assets": "Assets",
@@ -124,6 +130,9 @@ export default angular.module('thingsboard.locale', [])
                     "unassign-from-customer": "Unassign from customer",
                     "delete": "Delete asset",
                     "asset-public": "Asset is public",
+                    "asset-type": "Asset type",
+                    "asset-type-required": "Asset type is required.",
+                    "select-asset-type": "Select asset type",
                     "name": "Name",
                     "name-required": "Name is required.",
                     "description": "Description",
@@ -154,7 +163,10 @@ export default angular.module('thingsboard.locale', [])
                     "unassign-assets-title": "Are you sure you want to unassign { count, select, 1 {1 asset} other {# assets} }?",
                     "unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.",
                     "copyId": "Copy asset Id",
-                    "idCopiedMessage": "Asset Id has been copied to clipboard"
+                    "idCopiedMessage": "Asset Id has been copied to clipboard",
+                    "select-asset": "Select asset",
+                    "no-assets-matching": "No assets matching '{{entity}}' were found.",
+                    "asset-required": "Asset is required"
                 },
                 "attribute": {
                     "attributes": "Attributes",
@@ -166,6 +178,7 @@ export default angular.module('thingsboard.locale', [])
                     "scope-shared": "Shared attributes",
                     "add": "Add attribute",
                     "key": "Key",
+                    "last-update-time": "Last update time",
                     "key-required": "Attribute key is required.",
                     "value": "Value",
                     "value-required": "Attribute value is required.",
@@ -207,6 +220,7 @@ export default angular.module('thingsboard.locale', [])
                     "enter-search": "Enter search"
                 },
                 "customer": {
+                    "customer": "Customer",
                     "customers": "Customers",
                     "management": "Customer management",
                     "dashboard": "Customer Dashboard",
@@ -243,7 +257,10 @@ export default angular.module('thingsboard.locale', [])
                     "details": "Details",
                     "events": "Events",
                     "copyId": "Copy customer Id",
-                    "idCopiedMessage": "Customer Id has been copied to clipboard"
+                    "idCopiedMessage": "Customer Id has been copied to clipboard",
+                    "select-customer": "Select customer",
+                    "no-customers-matching": "No customers matching '{{entity}}' were found.",
+                    "customer-required": "Customer is required"
                 },
                 "datetime": {
                     "date-from": "Date from",
@@ -301,7 +318,7 @@ export default angular.module('thingsboard.locale', [])
                     "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
                     "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
                     "select-dashboard": "Select dashboard",
-                    "no-dashboards-matching": "No dashboards matching '{{dashboard}}' were found.",
+                    "no-dashboards-matching": "No dashboards matching '{{entity}}' were found.",
                     "dashboard-required": "Dashboard is required.",
                     "select-existing": "Select existing dashboard",
                     "create-new": "Create new dashboard",
@@ -328,6 +345,7 @@ export default angular.module('thingsboard.locale', [])
                     "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
                     "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
                     "display-title": "Display dashboard title",
+                    "toolbar-always-open": "Keep toolbar opened",
                     "title-color": "Title color",
                     "display-dashboards-selection": "Display dashboards selection",
                     "display-entities-selection": "Display entities selection",
@@ -421,7 +439,7 @@ export default angular.module('thingsboard.locale', [])
                     "create-new-key": "Create a new one!",
                     "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Device aliases must be unique whithin the dashboard.",
                     "configure-alias": "Configure '{{alias}}' alias",
-                    "no-devices-matching": "No devices matching '{{device}}' were found.",
+                    "no-devices-matching": "No devices matching '{{entity}}' were found.",
                     "alias": "Alias",
                     "alias-required": "Device alias is required.",
                     "remove-alias": "Remove device alias",
@@ -476,6 +494,9 @@ export default angular.module('thingsboard.locale', [])
                     "rsa-key-required": "RSA public key is required.",
                     "secret": "Secret",
                     "secret-required": "Secret is required.",
+                    "device-type": "Device type",
+                    "device-type-required": "Device type is required.",
+                    "select-device-type": "Select device type",
                     "name": "Name",
                     "name-required": "Name is required.",
                     "description": "Description",
@@ -490,7 +511,8 @@ export default angular.module('thingsboard.locale', [])
                     "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
                     "is-gateway": "Is gateway",
                     "public": "Public",
-                    "device-public": "Device is public"
+                    "device-public": "Device is public",
+                    "select-device": "Select device"
                 },
                 "dialog": {
                     "close": "Close dialog"
@@ -520,13 +542,18 @@ export default angular.module('thingsboard.locale', [])
                     "entity-list-empty": "No entities selected.",
                     "entity-name-filter-required": "Entity name filter is required.",
                     "entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
+                    "all-subtypes": "All",
                     "type": "Type",
+                    "type-required": "Entity type is required.",
                     "type-device": "Device",
                     "type-asset": "Asset",
                     "type-rule": "Rule",
                     "type-plugin": "Plugin",
                     "type-tenant": "Tenant",
                     "type-customer": "Customer",
+                    "type-user": "User",
+                    "type-dashboard": "Dashboard",
+                    "type-alarm": "Alarm",
                     "select-entities": "Select entities",
                     "no-aliases-found": "No aliases found.",
                     "no-alias-matching": "'{{alias}}' not found.",
@@ -664,7 +691,7 @@ export default angular.module('thingsboard.locale', [])
                     "system": "System",
                     "select-plugin": "Select plugin",
                     "plugin": "Plugin",
-                    "no-plugins-matching": "No plugins matching '{{plugin}}' were found.",
+                    "no-plugins-matching": "No plugins matching '{{entity}}' were found.",
                     "plugin-required": "Plugin is required.",
                     "plugin-require-match": "Please select an existing plugin.",
                     "events": "Events",
@@ -677,6 +704,7 @@ 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",
@@ -689,7 +717,38 @@ export default angular.module('thingsboard.locale', [])
                     "change-password": "Change Password",
                     "current-password": "Current password"
                 },
+                "relation": {
+                    "relations": "Relations",
+                    "direction": "Direction",
+                    "search-direction": {
+                        "FROM": "From",
+                        "TO": "To"
+                    },
+                    "from-relations": "Outbound relations",
+                    "to-relations": "Inbound relations",
+                    "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
+                    "type": "Type",
+                    "to-entity-type": "To entity type",
+                    "to-entity-name": "To entity name",
+                    "from-entity-type": "From entity type",
+                    "from-entity-name": "From entity name",
+                    "to-entity": "To entity",
+                    "from-entity": "From entity",
+                    "delete": "Delete relation",
+                    "relation-type": "Relation type",
+                    "relation-type-required": "Relation type is required.",
+                    "add": "Add relation",
+                    "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?",
+                    "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.",
+                    "delete-to-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
+                    "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.",
+                    "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?",
+                    "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.",
+                    "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
+                    "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities."
+                },
                 "rule": {
+                    "rule": "Rule",
                     "rules": "Rules",
                     "delete": "Delete rule",
                     "activate": "Activate rule",
@@ -741,12 +800,16 @@ export default angular.module('thingsboard.locale', [])
                     "rule-file": "Rule file",
                     "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.",
                     "copyId": "Copy rule Id",
-                    "idCopiedMessage": "Rule Id has been copied to clipboard"
+                    "idCopiedMessage": "Rule Id has been copied to clipboard",
+                    "select-rule": "Select rule",
+                    "no-rules-matching": "No rules matching '{{entity}}' were found.",
+                    "rule-required": "Rule is required"
                 },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
                 },
                 "tenant": {
+                    "tenant": "Tenant",
                     "tenants": "Tenants",
                     "management": "Tenant management",
                     "add": "Add Tenant",
@@ -767,7 +830,10 @@ export default angular.module('thingsboard.locale', [])
                     "details": "Details",
                     "events": "Events",
                     "copyId": "Copy tenant Id",
-                    "idCopiedMessage": "Tenant Id has been copied to clipboard"
+                    "idCopiedMessage": "Tenant Id has been copied to clipboard",
+                    "select-tenant": "Select tenant",
+                    "no-tenants-matching": "No tenants matching '{{entity}}' were found.",
+                    "tenant-required": "Tenant is required"
                 },
                 "timeinterval": {
                     "seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }",
@@ -795,6 +861,7 @@ export default angular.module('thingsboard.locale', [])
                     "time-period": "Time period"
                 },
                 "user": {
+                    "user": "User",
                     "users": "Users",
                     "customer-users": "Customer Users",
                     "tenant-admins": "Tenant Admins",
@@ -820,7 +887,10 @@ export default angular.module('thingsboard.locale', [])
                     "last-name": "Last Name",
                     "description": "Description",
                     "default-dashboard": "Default dashboard",
-                    "always-fullscreen": "Always fullscreen"
+                    "always-fullscreen": "Always fullscreen",
+                    "select-user": "Select user",
+                    "no-users-matching": "No users matching '{{entity}}' were found.",
+                    "user-required": "User is required"
                 },
                 "value": {
                     "type": "Value type",
diff --git a/ui/src/app/locale/locale.constant-es.js b/ui/src/app/locale/locale.constant-es.js
index 153019d..e4d4b49 100644
--- a/ui/src/app/locale/locale.constant-es.js
+++ b/ui/src/app/locale/locale.constant-es.js
@@ -235,7 +235,7 @@
               "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
               "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
               "select-dashboard": "Seleccionar panel",
-              "no-dashboards-matching": "Panel '{{dashboard}}' no encontrado.",
+              "no-dashboards-matching": "Panel '{{entity}}' no encontrado.",
               "dashboard-required": "Panel requerido.",
               "select-existing": "Seleccionar paneles existentes",
               "create-new": "Crear nuevo panel",
@@ -330,7 +330,7 @@
               "create-new-key": "Crear nueva clave!",
               "duplicate-alias-error": "Alias duplicado '{{alias}}'.<br> El alias de los dispositivos deben ser únicos dentro del panel.",
               "configure-alias": "Configurar alias '{{alias}}'",
-              "no-devices-matching": "No se encontró dispositivo '{{device}}'",
+              "no-devices-matching": "No se encontró dispositivo '{{entity}}'",
               "alias": "Alias",
               "alias-required": "Alias de dispositivo requerido.",
               "remove-alias": "Eliminar alias",
@@ -529,7 +529,7 @@
               "system": "Sistema",
               "select-plugin": "plugin",
               "plugin": "Plugin",
-              "no-plugins-matching": "No se encontraron plugins: '{{plugin}}'",
+              "no-plugins-matching": "No se encontraron plugins: '{{entity}}'",
               "plugin-required": "Plugin requerido.",
               "plugin-require-match": "Por favor, elija un plugin existente.",
               "events": "Eventos",
diff --git a/ui/src/app/locale/locale.constant-ko.js b/ui/src/app/locale/locale.constant-ko.js
index 8b2d4ba..32dbf49 100644
--- a/ui/src/app/locale/locale.constant-ko.js
+++ b/ui/src/app/locale/locale.constant-ko.js
@@ -218,7 +218,7 @@ export default function addLocaleKorean(locales) {
             "unassign-dashboards-title": "{ count, select, 1 {대시보드 1개} other {대시보드 #개} }의 할당을 취소하시겠습니까?",
             "unassign-dashboards-text": "선택된 대시보드가 할당 해제되고 커스터머는 액세스 할 수 없게됩니다.",
             "select-dashboard": "대시보드 선택",
-            "no-dashboards-matching": "'{{dashboard}}'와 일치하는 대시보드가 없습니다.",
+            "no-dashboards-matching": "'{{entity}}'와 일치하는 대시보드가 없습니다.",
             "dashboard-required": "대시보드를 입력하세요.",
             "select-existing": "기존 대시보드 선택",
             "create-new": "대시보드 생성",
@@ -305,7 +305,7 @@ export default function addLocaleKorean(locales) {
             "create-new-key": "새로 만들기!",
             "duplicate-alias-error": "중복된 '{{alias}}' 앨리어스가 있습니다.<br> 디바이스 앨리어스는 대시보드 내에서 고유해야 합니다.",
             "configure-alias": "'{{alias}}' 앨리어스 구성",
-            "no-devices-matching": "'{{device}}'와 일치하는 디바이스를 찾을 수 없습니다.",
+            "no-devices-matching": "'{{entity}}'와 일치하는 디바이스를 찾을 수 없습니다.",
             "alias": "앨리어스",
             "alias-required": "디바이스 앨리어스를 입력하세요.",
             "remove-alias": "디바이스 앨리어스 삭제",
@@ -496,7 +496,7 @@ export default function addLocaleKorean(locales) {
             "system": "시스템",
             "select-plugin": "플러그인 선택",
             "plugin": "플러그인",
-            "no-plugins-matching": "'{{plugin}}'과 일치하는 플러그인을 찾을 수 없습니다.",
+            "no-plugins-matching": "'{{entity}}'과 일치하는 플러그인을 찾을 수 없습니다.",
             "plugin-required": "플러그인을 입력하세요.",
             "plugin-require-match": "기존의 플러그인을 선택해주세요.",
             "events": "이벤트",
diff --git a/ui/src/app/locale/locale.constant-ru.js b/ui/src/app/locale/locale.constant-ru.js
index 47c9460..d7734b4 100644
--- a/ui/src/app/locale/locale.constant-ru.js
+++ b/ui/src/app/locale/locale.constant-ru.js
@@ -235,7 +235,7 @@ export default function addLocaleRussian(locales) {
             "socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard",
             "socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard",
             "select-dashboard": "Выберите дашборд",
-            "no-dashboards-matching": "Дашборд '{{dashboard}}' не найден.",
+            "no-dashboards-matching": "Дашборд '{{entity}}' не найден.",
             "dashboard-required": "Дашборд обязателен.",
             "select-existing": "Выберите существующий дашборд",
             "create-new": "Создать новый дашборд",
@@ -330,7 +330,7 @@ export default function addLocaleRussian(locales) {
             "create-new-key": "Создать новый!",
             "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.<br>В рамках дашборда псевдонимы устройств должны быть уникальными.",
             "configure-alias": "Конфигурировать '{{alias}}' псевдоним",
-            "no-devices-matching": "Устройство '{{device}}' не найдено.",
+            "no-devices-matching": "Устройство '{{entity}}' не найдено.",
             "alias": "Псевдоним",
             "alias-required": "Псевдоним устройства обязателен.",
             "remove-alias": "Удалить псевдоним устройства",
@@ -529,7 +529,7 @@ export default function addLocaleRussian(locales) {
             "system": "Системный",
             "select-plugin": "Выберите плагин",
             "plugin": "Плагин",
-            "no-plugins-matching": "Плагин '{{plugin}}' не найден.",
+            "no-plugins-matching": "Плагин '{{entity}}' не найден.",
             "plugin-required": "Плагин обязателен.",
             "plugin-require-match": "Пожалуйста, выберите существующий плагин.",
             "events": "События",
diff --git a/ui/src/app/locale/locale.constant-zh.js b/ui/src/app/locale/locale.constant-zh.js
index ba8e3c0..3246070 100644
--- a/ui/src/app/locale/locale.constant-zh.js
+++ b/ui/src/app/locale/locale.constant-zh.js
@@ -235,7 +235,7 @@ export default function addLocaleChinese(locales) {
             "socialshare-text" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
             "socialshare-title" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
             "select-dashboard" : "选择仪表板",
-            "no-dashboards-matching" : "找不到符合 '{{dashboard}}' 的仪表板。",
+            "no-dashboards-matching" : "找不到符合 '{{entity}}' 的仪表板。",
             "dashboard-required" : "仪表板是必需的。",
             "select-existing" : "选择现有仪表板",
             "create-new" : "创建新的仪表板",
@@ -330,7 +330,7 @@ export default function addLocaleChinese(locales) {
             "create-new-key": "创建一个新的!",
             "duplicate-alias-error" : "找到重复别名 '{{alias}}'。 <br> 设备别名必须是唯一的。",
             "configure-alias" : "配置 '{{alias}}' 别名",
-            "no-devices-matching" : "找不到与 '{{device}}' 匹配的设备。",
+            "no-devices-matching" : "找不到与 '{{entity}}' 匹配的设备。",
             "alias" : "别名",
             "alias-required" : "需要设备别名。",
             "remove-alias": "删除设备别名",
@@ -529,7 +529,7 @@ export default function addLocaleChinese(locales) {
             "system" : "系统",
             "select-plugin" : "选择插件",
             "plugin" : "插件",
-            "no-plugins-matching" : "没有找到匹配'{{plugin}}'的插件。",
+            "no-plugins-matching" : "没有找到匹配'{{entity}}'的插件。",
             "plugin-required" : "插件是必需的。",
             "plugin-require-match" : "请选择一个现有的插件。",
             "events" : "事件",
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
index bd8cf74..d04ebcb 100644
--- a/ui/src/app/plugin/plugins.tpl.html
+++ b/ui/src/app/plugin/plugins.tpl.html
@@ -56,5 +56,11 @@
                             disabled-event-types="{{vm.types.eventType.alarm.value}}">
             </tb-event-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+            <tb-relation-table flex
+                               entity-id="vm.grid.operatingItem().id.id"
+                               entity-type="{{vm.types.entityType.plugin}}">
+            </tb-relation-table>
+        </md-tab>
     </md-tabs>
 </tb-grid>
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
index bd7ea48..16097cc 100644
--- a/ui/src/app/rule/rules.tpl.html
+++ b/ui/src/app/rule/rules.tpl.html
@@ -56,5 +56,11 @@
                             disabled-event-types="{{vm.types.eventType.alarm.value}}">
             </tb-event-table>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+            <tb-relation-table flex
+                               entity-id="vm.grid.operatingItem().id.id"
+                               entity-type="{{vm.types.entityType.rule}}">
+            </tb-relation-table>
+        </md-tab>
     </md-tabs>
 </tb-grid>
diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html
index ada2a3f..00350a4 100644
--- a/ui/src/app/tenant/tenants.tpl.html
+++ b/ui/src/app/tenant/tenants.tpl.html
@@ -53,5 +53,11 @@
 							default-event-type="{{vm.types.eventType.alarm.value}}">
 			</tb-event-table>
 		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'relation.relations' | translate }}">
+			<tb-relation-table flex
+							   entity-id="vm.grid.operatingItem().id.id"
+							   entity-type="{{vm.types.entityType.tenant}}">
+			</tb-relation-table>
+		</md-tab>
 	</md-tabs>
 </tb-grid>
diff --git a/ui/src/scss/constants.scss b/ui/src/scss/constants.scss
index 73697fc..c56d40b 100644
--- a/ui/src/scss/constants.scss
+++ b/ui/src/scss/constants.scss
@@ -20,10 +20,12 @@
 $gray: #eee;
 
 $primary-palette-color: 'indigo';
+$default: '500';
 $hue-1: '300';
 $hue-2: '800';
 $hue-3: 'a100';
 
+$primary-default: #305680; //material-color($primary-palette-color, $default);
 $primary-hue-1: material-color($primary-palette-color, $hue-1);
 $primary-hue-2: material-color($primary-palette-color, $hue-2);
 $primary-hue-3: rgb(207, 216, 220);
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 4ffabc9..dbe3543 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -261,6 +261,45 @@ pre.tb-highlight {
   font-size: 16px;
 }
 
+.tb-data-table {
+  md-toolbar {
+    z-index: 0;
+  }
+  span.no-data-found {
+    position: relative;
+    height: calc(100% - 57px);
+    text-transform: uppercase;
+    display: flex;
+  }
+  table.md-table {
+    tbody {
+      tr {
+        td {
+          &.tb-action-cell {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            min-width: 72px;
+            max-width: 72px;
+            width: 72px;
+            .md-button {
+              &.md-icon-button {
+                margin: 0;
+                padding: 6px;
+                width: 36px;
+                height: 36px;
+              }
+            }
+            .tb-spacer {
+              padding-left: 38px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
 
 /***********************
  * Flow
@@ -397,6 +436,7 @@ md-tabs.tb-headless {
  ***********************/
 
 section.tb-header-buttons {
+  pointer-events: none;
   position: absolute;
   right: 0px;
   top: 86px;