thingsboard-developers

Merge remote-tracking branch 'origin/asset-alarm-mgmt'

5/18/2017 10:58:04 PM

Changes

.gitignore 1(+1 -0)

tools/pom.xml 36(+0 -36)

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

ui/src/app/app.js 12(+12 -0)

ui/src/app/app.run.js 52(+42 -10)

Details

.gitignore 1(+1 -0)

diff --git a/.gitignore b/.gitignore
index 6f6dd61..9039e8e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+*.toDelete
 output/**
 *.class
 *~
diff --git a/application/.gitignore b/application/.gitignore
index 08eb0a0..b5246c6 100644
--- a/application/.gitignore
+++ b/application/.gitignore
@@ -1 +1,2 @@
-!bin/
\ No newline at end of file
+!bin/
+/bin/
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 7cc2c9f..ae9d967 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
+import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.attributes.AttributesService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.device.DeviceService;
@@ -81,6 +82,9 @@ public class ActorSystemContext {
     @Getter private DeviceService deviceService;
 
     @Autowired
+    @Getter private AssetService assetService;
+
+    @Autowired
     @Getter private TenantService tenantService;
 
     @Autowired
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index d4c42d8..0c73470 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -51,13 +51,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
 import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
 
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
+import java.util.*;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
@@ -205,25 +199,21 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
 
     void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
         refreshAttributes(msg);
-        Set<AttributeKey> keys = msg.getDeletedKeys();
         if (attributeSubscriptions.size() > 0) {
             ToDeviceMsg notification = null;
             if (msg.isDeleted()) {
-                List<AttributeKey> sharedKeys = keys.stream()
+                List<AttributeKey> sharedKeys = msg.getDeletedKeys().stream()
                         .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
                         .collect(Collectors.toList());
                 notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys));
             } else {
-                List<AttributeKvEntry> attributes = keys.stream()
-                        .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
-                        .map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey()))
-                        .filter(Optional::isPresent)
-                        .map(Optional::get)
-                        .collect(Collectors.toList());
-                if (attributes.size() > 0) {
-                    notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
-                } else {
-                    logger.debug("[{}] No public server side attributes changed!", deviceId);
+                if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) {
+                    List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues());
+                    if (attributes.size() > 0) {
+                        notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
+                    } else {
+                        logger.debug("[{}] No public server side attributes changed!", deviceId);
+                    }
                 }
             }
             if (notification != null) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
index 0b9e37c..33dfa3f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -24,17 +24,19 @@ import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.id.CustomerId;
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.kv.AttributeKey;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.common.data.kv.TsKvQuery;
 import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
 import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
@@ -89,103 +91,107 @@ public final class PluginProcessingContext implements PluginContext {
     }
 
     @Override
-    public void saveAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List<AttributeKvEntry> attributes, final PluginCallback<Void> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes);
+    public void saveAttributes(final TenantId tenantId, final EntityId entityId, final String scope, final List<AttributeKvEntry> attributes, final PluginCallback<Void> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(entityId, scope, attributes);
             Futures.addCallback(rsListFuture, getListCallback(callback, v -> {
-                onDeviceAttributesChanged(tenantId, deviceId, scope, attributes);
+                if (entityId.getEntityType() == EntityType.DEVICE) {
+                    onDeviceAttributesChanged(tenantId, new DeviceId(entityId.getId()), scope, attributes);
+                }
                 return null;
             }), executor);
         }));
     }
 
     @Override
-    public void removeAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List<String> keys, final PluginCallback<Void> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<ResultSet>> future = pluginCtx.attributesService.removeAll(deviceId, scope, keys);
+    public void removeAttributes(final TenantId tenantId, final EntityId entityId, final String scope, final List<String> keys, final PluginCallback<Void> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<ResultSet>> future = pluginCtx.attributesService.removeAll(entityId, scope, keys);
             Futures.addCallback(future, getCallback(callback, v -> null), executor);
-            onDeviceAttributesDeleted(tenantId, deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet()));
+            if (entityId.getEntityType() == EntityType.DEVICE) {
+                onDeviceAttributesDeleted(tenantId, new DeviceId(entityId.getId()), keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet()));
+            }
         }));
     }
 
     @Override
-    public void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, final PluginCallback<Optional<AttributeKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<Optional<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
+    public void loadAttribute(EntityId entityId, String attributeType, String attributeKey, final PluginCallback<Optional<AttributeKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<Optional<AttributeKvEntry>> future = pluginCtx.attributesService.find(entityId, attributeType, attributeKey);
             Futures.addCallback(future, getCallback(callback, v -> v), executor);
         }));
     }
 
     @Override
-    public void loadAttributes(DeviceId deviceId, String attributeType, Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys);
+    public void loadAttributes(EntityId entityId, String attributeType, Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.find(entityId, attributeType, attributeKeys);
             Futures.addCallback(future, getCallback(callback, v -> v), executor);
         }));
     }
 
     @Override
-    public void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.findAll(deviceId, attributeType);
+    public void loadAttributes(EntityId entityId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.findAll(entityId, attributeType);
             Futures.addCallback(future, getCallback(callback, v -> v), executor);
         }));
     }
 
     @Override
-    public void loadAttributes(final DeviceId deviceId, final Collection<String> attributeTypes, final PluginCallback<List<AttributeKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
+    public void loadAttributes(final EntityId entityId, final Collection<String> attributeTypes, final PluginCallback<List<AttributeKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
             List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
-            attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.findAll(deviceId, attributeType)));
+            attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.findAll(entityId, attributeType)));
             convertFuturesAndAddCallback(callback, futures);
         }));
     }
 
     @Override
-    public void loadAttributes(final DeviceId deviceId, final Collection<String> attributeTypes, final Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
+    public void loadAttributes(final EntityId entityId, final Collection<String> attributeTypes, final Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
             List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
-            attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys)));
+            attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.find(entityId, attributeType, attributeKeys)));
             convertFuturesAndAddCallback(callback, futures);
         }));
     }
 
     @Override
-    public void saveTsData(final DeviceId deviceId, final TsKvEntry entry, final PluginCallback<Void> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry);
+    public void saveTsData(final EntityId entityId, final TsKvEntry entry, final PluginCallback<Void> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(entityId, entry);
             Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
         }));
     }
 
     @Override
-    public void saveTsData(final DeviceId deviceId, final List<TsKvEntry> entries, final PluginCallback<Void> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries);
+    public void saveTsData(final EntityId entityId, final List<TsKvEntry> entries, final PluginCallback<Void> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(entityId, entries);
             Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
         }));
     }
 
     @Override
-    public void loadTimeseries(final DeviceId deviceId, final List<TsKvQuery> queries, final PluginCallback<List<TsKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<TsKvEntry>> future = pluginCtx.tsService.findAll(DataConstants.DEVICE, deviceId, queries);
+    public void loadTimeseries(final EntityId entityId, final List<TsKvQuery> queries, final PluginCallback<List<TsKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<TsKvEntry>> future = pluginCtx.tsService.findAll(entityId, queries);
             Futures.addCallback(future, getCallback(callback, v -> v), executor);
         }));
     }
 
     @Override
-    public void loadLatestTimeseries(final DeviceId deviceId, final PluginCallback<List<TsKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId);
+    public void loadLatestTimeseries(final EntityId entityId, final PluginCallback<List<TsKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ResultSetFuture future = pluginCtx.tsService.findAllLatest(entityId);
             Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor);
         }));
     }
 
     @Override
-    public void loadLatestTimeseries(final DeviceId deviceId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
-        validate(deviceId, new ValidationCallback(callback, ctx -> {
-            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(DataConstants.DEVICE, deviceId, keys);
+    public void loadLatestTimeseries(final EntityId entityId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
+        validate(entityId, new ValidationCallback(callback, ctx -> {
+            ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(entityId, keys);
             Futures.addCallback(rsListFuture, getListCallback(callback, rsList ->
             {
                 List<TsKvEntry> result = new ArrayList<>();
@@ -268,24 +274,101 @@ public final class PluginProcessingContext implements PluginContext {
         validate(deviceId, new ValidationCallback(callback, ctx -> callback.onSuccess(ctx, null)));
     }
 
-    private void validate(DeviceId deviceId, ValidationCallback callback) {
+    private void validate(EntityId entityId, ValidationCallback callback) {
         if (securityCtx.isPresent()) {
             final PluginApiCallSecurityContext ctx = securityCtx.get();
-            if (ctx.isTenantAdmin() || ctx.isCustomerUser()) {
-                ListenableFuture<Device> deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(deviceId);
-                Futures.addCallback(deviceFuture, getCallback(callback, device -> {
-                    if (device == null) {
-                        return Boolean.FALSE;
-                    } else {
-                        if (!device.getTenantId().equals(ctx.getTenantId())) {
-                            return Boolean.FALSE;
-                        } else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) {
-                            return Boolean.FALSE;
+            if (ctx.isTenantAdmin() || ctx.isCustomerUser() || ctx.isSystemAdmin()) {
+                switch (entityId.getEntityType()) {
+                    case DEVICE:
+                        if (ctx.isSystemAdmin()) {
+                            callback.onSuccess(this, Boolean.FALSE);
                         } else {
-                            return Boolean.TRUE;
+                            ListenableFuture<Device> deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
+                            Futures.addCallback(deviceFuture, getCallback(callback, device -> {
+                                if (device == null) {
+                                    return Boolean.FALSE;
+                                } else {
+                                    if (!device.getTenantId().equals(ctx.getTenantId())) {
+                                        return Boolean.FALSE;
+                                    } else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) {
+                                        return Boolean.FALSE;
+                                    } else {
+                                        return Boolean.TRUE;
+                                    }
+                                }
+                            }));
                         }
-                    }
-                }));
+                        return;
+                    case ASSET:
+                        if (ctx.isSystemAdmin()) {
+                            callback.onSuccess(this, Boolean.FALSE);
+                        } else {
+                            ListenableFuture<Asset> assetFuture = pluginCtx.assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
+                            Futures.addCallback(assetFuture, getCallback(callback, asset -> {
+                                if (asset == null) {
+                                    return Boolean.FALSE;
+                                } else {
+                                    if (!asset.getTenantId().equals(ctx.getTenantId())) {
+                                        return Boolean.FALSE;
+                                    } else if (ctx.isCustomerUser() && !asset.getCustomerId().equals(ctx.getCustomerId())) {
+                                        return Boolean.FALSE;
+                                    } else {
+                                        return Boolean.TRUE;
+                                    }
+                                }
+                            }));
+                        }
+                        return;
+                    case RULE:
+                        if (ctx.isCustomerUser()) {
+                            callback.onSuccess(this, Boolean.FALSE);
+                        } else {
+                            ListenableFuture<RuleMetaData> ruleFuture = pluginCtx.ruleService.findRuleByIdAsync(new RuleId(entityId.getId()));
+                            Futures.addCallback(ruleFuture, getCallback(callback, rule -> rule != null && rule.getTenantId().equals(ctx.getTenantId())));
+                        }
+                        return;
+                    case PLUGIN:
+                        if (ctx.isCustomerUser()) {
+                            callback.onSuccess(this, Boolean.FALSE);
+                        } else {
+                            ListenableFuture<PluginMetaData> pluginFuture = pluginCtx.pluginService.findPluginByIdAsync(new PluginId(entityId.getId()));
+                            Futures.addCallback(pluginFuture, getCallback(callback, plugin -> plugin != null && plugin.getTenantId().equals(ctx.getTenantId())));
+                        }
+                        return;
+                    case CUSTOMER:
+                        if (ctx.isSystemAdmin()) {
+                            callback.onSuccess(this, Boolean.FALSE);
+                        } else {
+                            ListenableFuture<Customer> customerFuture = pluginCtx.customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
+                            Futures.addCallback(customerFuture, getCallback(callback, customer -> {
+                                if (customer == null) {
+                                    return Boolean.FALSE;
+                                } else {
+                                    if (!customer.getTenantId().equals(ctx.getTenantId())) {
+                                        return Boolean.FALSE;
+                                    } else if (ctx.isCustomerUser() && !customer.getId().equals(ctx.getCustomerId())) {
+                                        return Boolean.FALSE;
+                                    } else {
+                                        return Boolean.TRUE;
+                                    }
+                                }
+                            }));
+                        }
+                        return;
+                    case TENANT:
+                        if (ctx.isCustomerUser()) {
+                            callback.onSuccess(this, Boolean.FALSE);
+                        } else if (ctx.isSystemAdmin()) {
+                            callback.onSuccess(this, Boolean.TRUE);
+                        } else {
+                            ListenableFuture<Tenant> tenantFuture = pluginCtx.tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+                            Futures.addCallback(tenantFuture, getCallback(callback, tenant -> tenant != null && tenant.getId().equals(ctx.getTenantId())));
+                        }
+                        return;
+                    default:
+                        //TODO: add support of other entities
+                        throw new IllegalStateException("Not Implemented!");
+                }
             } else {
                 callback.onSuccess(this, Boolean.FALSE);
             }
@@ -295,8 +378,8 @@ public final class PluginProcessingContext implements PluginContext {
     }
 
     @Override
-    public Optional<ServerAddress> resolve(DeviceId deviceId) {
-        return pluginCtx.routingService.resolve(deviceId);
+    public Optional<ServerAddress> resolve(EntityId entityId) {
+        return pluginCtx.routingService.resolveById(entityId);
     }
 
     @Override
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
index b09b72e..43ddce6 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -25,8 +25,13 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
 import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
 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.timeseries.TimeseriesService;
 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
@@ -46,7 +51,12 @@ public final class SharedPluginProcessingContext {
     final ActorRef currentActor;
     final ActorSystemContext systemContext;
     final PluginWebSocketMsgEndpoint msgEndpoint;
+    final AssetService assetService;
     final DeviceService deviceService;
+    final RuleService ruleService;
+    final PluginService pluginService;
+    final CustomerService customerService;
+    final TenantService tenantService;
     final TimeseriesService tsService;
     final AttributesService attributesService;
     final ClusterRpcService rpcService;
@@ -65,9 +75,14 @@ public final class SharedPluginProcessingContext {
         this.msgEndpoint = sysContext.getWsMsgEndpoint();
         this.tsService = sysContext.getTsService();
         this.attributesService = sysContext.getAttributesService();
+        this.assetService = sysContext.getAssetService();
         this.deviceService = sysContext.getDeviceService();
         this.rpcService = sysContext.getRpcService();
         this.routingService = sysContext.getRoutingService();
+        this.ruleService = sysContext.getRuleService();
+        this.pluginService = sysContext.getPluginService();
+        this.customerService = sysContext.getCustomerService();
+        this.tenantService = sysContext.getTenantService();
     }
 
     public PluginId getPluginId() {
@@ -89,7 +104,7 @@ public final class SharedPluginProcessingContext {
     }
 
     private <T> void forward(DeviceId deviceId, T msg, BiConsumer<ServerAddress, T> rpcFunction) {
-        Optional<ServerAddress> instance = routingService.resolve(deviceId);
+        Optional<ServerAddress> instance = routingService.resolveById(deviceId);
         if (instance.isPresent()) {
             log.trace("[{}] Forwarding msg {} to remote device actor!", pluginId, msg);
             rpcFunction.accept(instance.get(), msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java b/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java
index 707afa5..c8d79b4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java
@@ -38,7 +38,7 @@ public class ValidationCallback implements PluginCallback<Boolean> {
         if (value) {
             action.accept(ctx);
         } else {
-            onFailure(ctx, new UnauthorizedException());
+            onFailure(ctx, new UnauthorizedException("Permission denied."));
         }
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index 54160bc..65d2c69 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -230,7 +230,7 @@ public class DefaultActorService implements ActorService {
     @Override
     public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
         DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
-        Optional<ServerAddress> address = actorContext.getRoutingService().resolve(deviceId);
+        Optional<ServerAddress> address = actorContext.getRoutingService().resolveById(deviceId);
         if (address.isPresent()) {
             rpcService.tell(address.get(), msg);
         } else {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
index b8d13ec..483c034 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
@@ -81,13 +81,13 @@ abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgP
     }
 
     protected Optional<ServerAddress> forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward) {
-        Optional<ServerAddress> address = systemContext.getRoutingService().resolve(toForward.getDeviceId());
+        Optional<ServerAddress> address = systemContext.getRoutingService().resolveById(toForward.getDeviceId());
         forwardToAppActor(ctx, toForward, address);
         return address;
     }
 
     protected Optional<ServerAddress> forwardToAppActorIfAdressChanged(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
-        Optional<ServerAddress> newAddress = systemContext.getRoutingService().resolve(toForward.getDeviceId());
+        Optional<ServerAddress> newAddress = systemContext.getRoutingService().resolveById(toForward.getDeviceId());
         if (!newAddress.equals(oldAddress)) {
             if (newAddress.isPresent()) {
                 systemContext.getRpcService().tell(newAddress.get(),
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
index f806687..b451d77 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
@@ -116,7 +116,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     @Override
     public void processClusterEvent(ActorContext context, ClusterEventMsg msg) {
         if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
-            Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
+            Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolveById(getDeviceId());
             if (!newTargetServer.equals(currentTargetServer)) {
                 firstMsg = true;
                 currentTargetServer = newTargetServer;
diff --git a/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java b/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java
new file mode 100644
index 0000000..62b4ec2
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/config/MvcCorsProperties.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by yyh on 2017/5/2.
+ * CORS configuration
+ */
+@Configuration
+@ConfigurationProperties(prefix = "spring.mvc.cors")
+public class MvcCorsProperties {
+
+    private Map<String, CorsConfiguration> mappings = new HashMap<>();
+
+    public MvcCorsProperties() {
+    }
+
+    public Map<String, CorsConfiguration> getMappings() {
+        return mappings;
+    }
+
+    public void setMappings(Map<String, CorsConfiguration> mappings) {
+        this.mappings = mappings;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index a7918e0..cdca099 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -18,7 +18,9 @@ package org.thingsboard.server.config;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.annotation.Order;
@@ -34,11 +36,15 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.web.cors.CorsUtils;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationProvider;
 import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilter;
 import org.thingsboard.server.service.security.auth.jwt.*;
 import org.thingsboard.server.service.security.auth.jwt.extractor.TokenExtractor;
+import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -56,6 +62,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
     public static final String WEBJARS_ENTRY_POINT = "/webjars/**";
     public static final String DEVICE_API_ENTRY_POINT = "/api/v1/**";
     public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
+    public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public";
     public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
     public static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"};
     public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
@@ -88,9 +95,17 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
     }
 
     @Bean
+    protected RestPublicLoginProcessingFilter buildRestPublicLoginProcessingFilter() throws Exception {
+        RestPublicLoginProcessingFilter filter = new RestPublicLoginProcessingFilter(PUBLIC_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
+        filter.setAuthenticationManager(this.authenticationManager);
+        return filter;
+    }
+
+    @Bean
     protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
         List<String> pathsToSkip = new ArrayList(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS));
-        pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
+        pathsToSkip.addAll(Arrays.asList(WS_TOKEN_BASED_AUTH_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT,
+                PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, WEBJARS_ENTRY_POINT));
         SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
         JwtTokenAuthenticationProcessingFilter filter
                 = new JwtTokenAuthenticationProcessingFilter(failureHandler, jwtHeaderTokenExtractor, matcher);
@@ -136,6 +151,8 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
     protected void configure(HttpSecurity http) throws Exception {
         http.headers().cacheControl().disable().frameOptions().disable()
                 .and()
+                .cors()
+                .and()
                 .csrf().disable()
                 .exceptionHandling()
                 .and()
@@ -146,6 +163,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
                 .antMatchers(WEBJARS_ENTRY_POINT).permitAll() // Webjars
                 .antMatchers(DEVICE_API_ENTRY_POINT).permitAll() // Device HTTP Transport API
                 .antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
+                .antMatchers(PUBLIC_LOGIN_ENTRY_POINT).permitAll() // Public login end-point
                 .antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll() // Token refresh end-point
                 .antMatchers(NON_TOKEN_BASED_AUTH_ENTRY_POINTS).permitAll() // static resources, user activation and password reset end-points
                 .and()
@@ -156,8 +174,22 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
                 .exceptionHandling().accessDeniedHandler(restAccessDeniedHandler)
                 .and()
                 .addFilterBefore(buildRestLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
+                .addFilterBefore(buildRestPublicLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
                 .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
                 .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
                 .addFilterBefore(buildWsJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
     }
+
+
+    @Bean
+    @ConditionalOnMissingBean(CorsFilter.class)
+    public CorsFilter corsFilter(@Autowired MvcCorsProperties mvcCorsProperties) {
+        if (mvcCorsProperties.getMappings().size() == 0) {
+            return new CorsFilter(new UrlBasedCorsConfigurationSource());
+        } else {
+            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+            source.setCorsConfigurations(mvcCorsProperties.getMappings());
+            return new CorsFilter(source);
+        }
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
new file mode 100644
index 0000000..24497d4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -0,0 +1,234 @@
+/**
+ * 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.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.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+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.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 AssetController extends BaseController {
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.GET)
+    @ResponseBody
+    public Asset getAssetById(@PathVariable("assetId") String strAssetId) throws ThingsboardException {
+        checkParameter("assetId", strAssetId);
+        try {
+            AssetId assetId = new AssetId(toUUID(strAssetId));
+            return checkAssetId(assetId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/asset", method = RequestMethod.POST)
+    @ResponseBody
+    public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException {
+        try {
+            asset.setTenantId(getCurrentUser().getTenantId());
+            return checkNotNull(assetService.saveAsset(asset));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE)
+    @ResponseStatus(value = HttpStatus.OK)
+    public void deleteAsset(@PathVariable("assetId") String strAssetId) throws ThingsboardException {
+        checkParameter("assetId", strAssetId);
+        try {
+            AssetId assetId = new AssetId(toUUID(strAssetId));
+            checkAssetId(assetId);
+            assetService.deleteAsset(assetId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/customer/{customerId}/asset/{assetId}", method = RequestMethod.POST)
+    @ResponseBody
+    public Asset assignAssetToCustomer(@PathVariable("customerId") String strCustomerId,
+                                       @PathVariable("assetId") String strAssetId) throws ThingsboardException {
+        checkParameter("customerId", strCustomerId);
+        checkParameter("assetId", strAssetId);
+        try {
+            CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+            checkCustomerId(customerId);
+
+            AssetId assetId = new AssetId(toUUID(strAssetId));
+            checkAssetId(assetId);
+
+            return checkNotNull(assetService.assignAssetToCustomer(assetId, customerId));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/customer/asset/{assetId}", method = RequestMethod.DELETE)
+    @ResponseBody
+    public Asset unassignAssetFromCustomer(@PathVariable("assetId") String strAssetId) throws ThingsboardException {
+        checkParameter("assetId", strAssetId);
+        try {
+            AssetId assetId = new AssetId(toUUID(strAssetId));
+            Asset asset = checkAssetId(assetId);
+            if (asset.getCustomerId() == null || asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+                throw new IncorrectParameterException("Asset isn't assigned to any customer!");
+            }
+            return checkNotNull(assetService.unassignAssetFromCustomer(assetId));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/customer/public/asset/{assetId}", method = RequestMethod.POST)
+    @ResponseBody
+    public Asset assignAssetToPublicCustomer(@PathVariable("assetId") String strAssetId) throws ThingsboardException {
+        checkParameter("assetId", strAssetId);
+        try {
+            AssetId assetId = new AssetId(toUUID(strAssetId));
+            Asset asset = checkAssetId(assetId);
+            Customer publicCustomer = customerService.findOrCreatePublicCustomer(asset.getTenantId());
+            return checkNotNull(assetService.assignAssetToCustomer(assetId, publicCustomer.getId()));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/tenant/assets", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TextPageData<Asset> getTenantAssets(
+            @RequestParam int limit,
+            @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));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET)
+    @ResponseBody
+    public Asset getTenantAsset(
+            @RequestParam String assetName) throws ThingsboardException {
+        try {
+            TenantId tenantId = getCurrentUser().getTenantId();
+            return checkNotNull(assetService.findAssetByTenantIdAndName(tenantId, assetName));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/customer/{customerId}/assets", params = {"limit"}, method = RequestMethod.GET)
+    @ResponseBody
+    public TextPageData<Asset> getCustomerAssets(
+            @PathVariable("customerId") String strCustomerId,
+            @RequestParam int limit,
+            @RequestParam(required = false) String textSearch,
+            @RequestParam(required = false) String idOffset,
+            @RequestParam(required = false) String textOffset) throws ThingsboardException {
+        checkParameter("customerId", strCustomerId);
+        try {
+            TenantId tenantId = getCurrentUser().getTenantId();
+            CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+            checkCustomerId(customerId);
+            TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset);
+            return checkNotNull(assetService.findAssetsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET)
+    @ResponseBody
+    public List<Asset> getAssetsByIds(
+            @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException {
+        checkArrayParameter("assetIds", strAssetIds);
+        try {
+            SecurityUser user = getCurrentUser();
+            TenantId tenantId = user.getTenantId();
+            CustomerId customerId = user.getCustomerId();
+            List<AssetId> assetIds = new ArrayList<>();
+            for (String strAssetId : strAssetIds) {
+                assetIds.add(new AssetId(toUUID(strAssetId)));
+            }
+            ListenableFuture<List<Asset>> assets;
+            if (customerId == null || customerId.isNullUid()) {
+                assets = assetService.findAssetsByTenantIdAndIdsAsync(tenantId, assetIds);
+            } else {
+                assets = assetService.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, assetIds);
+            }
+            return checkNotNull(assets.get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/assets", method = RequestMethod.POST)
+    @ResponseBody
+    public List<Asset> findByQuery(@RequestBody AssetSearchQuery query) throws ThingsboardException {
+        checkNotNull(query);
+        checkNotNull(query.getParameters());
+        checkNotNull(query.getAssetTypes());
+        checkEntityId(query.getParameters().getEntityId());
+        try {
+            List<Asset> assets = checkNotNull(assetService.findAssetsByQuery(query).get());
+            assets = assets.stream().filter(asset -> {
+                try {
+                    checkAsset(asset);
+                    return true;
+                } catch (ThingsboardException e) {
+                    return false;
+                }
+            }).collect(Collectors.toList());
+            return assets;
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index 5ef52c2..d06f2be 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.service.mail.MailService;
 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
 import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
 import org.thingsboard.server.service.security.model.token.JwtToken;
 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 
@@ -167,7 +168,8 @@ public class AuthController extends BaseController {
             String encodedPassword = passwordEncoder.encode(password);
             UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
             User user = userService.findUserById(credentials.getUserId());
-            SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled());
+            UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
+            SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal);
             String baseUrl = constructBaseUrl(request);
             String loginUrl = String.format("%s/login", baseUrl);
             String email = user.getEmail();
@@ -201,7 +203,8 @@ public class AuthController extends BaseController {
                 userCredentials.setResetToken(null);
                 userCredentials = userService.saveUserCredentials(userCredentials);
                 User user = userService.findUserById(userCredentials.getUserId());
-                SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled());
+                UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
+                SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), principal);
                 String baseUrl = constructBaseUrl(request);
                 String loginUrl = String.format("%s/login", baseUrl);
                 String email = user.getEmail();
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 034206f..12d43e0 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -22,10 +22,8 @@ import org.springframework.security.core.Authentication;
 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.Customer;
-import org.thingsboard.server.common.data.Dashboard;
-import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.asset.Asset;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.page.TimePageLink;
@@ -36,6 +34,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.asset.AssetService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceCredentialsService;
@@ -44,6 +43,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.dao.widget.WidgetTypeService;
@@ -79,6 +79,9 @@ public abstract class BaseController {
     protected DeviceService deviceService;
 
     @Autowired
+    protected AssetService assetService;
+
+    @Autowired
     protected DeviceCredentialsService deviceCredentialsService;
 
     @Autowired
@@ -102,6 +105,9 @@ public abstract class BaseController {
     @Autowired
     protected ActorService actorService;
 
+    @Autowired
+    protected RelationService relationService;
+
 
     @ExceptionHandler(ThingsboardException.class)
     public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
@@ -251,6 +257,43 @@ public abstract class BaseController {
         }
     }
 
+    protected void checkEntityId(EntityId entityId) throws ThingsboardException {
+        try {
+            checkNotNull(entityId);
+            validateId(entityId.getId(), "Incorrect entityId " + entityId);
+            switch (entityId.getEntityType()) {
+                case DEVICE:
+                    checkDevice(deviceService.findDeviceById(new DeviceId(entityId.getId())));
+                    return;
+                case CUSTOMER:
+                    checkCustomerId(new CustomerId(entityId.getId()));
+                    return;
+                case TENANT:
+                    checkTenantId(new TenantId(entityId.getId()));
+                    return;
+                case PLUGIN:
+                    checkPlugin(new PluginId(entityId.getId()));
+                    return;
+                case RULE:
+                    checkRule(new RuleId(entityId.getId()));
+                    return;
+                case ASSET:
+                    checkAsset(assetService.findAssetById(new AssetId(entityId.getId())));
+                    return;
+                case DASHBOARD:
+                    checkDashboardId(new DashboardId(entityId.getId()));
+                    return;
+                case USER:
+                    checkUserId(new UserId(entityId.getId()));
+                    return;
+                default:
+                    throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType());
+            }
+        } catch (Exception e) {
+            throw handleException(e, false);
+        }
+    }
+
     Device checkDeviceId(DeviceId deviceId) throws ThingsboardException {
         try {
             validateId(deviceId, "Incorrect deviceId " + deviceId);
@@ -270,6 +313,25 @@ public abstract class BaseController {
         }
     }
 
+    Asset checkAssetId(AssetId assetId) throws ThingsboardException {
+        try {
+            validateId(assetId, "Incorrect assetId " + assetId);
+            Asset asset = assetService.findAssetById(assetId);
+            checkAsset(asset);
+            return asset;
+        } catch (Exception e) {
+            throw handleException(e, false);
+        }
+    }
+
+    protected void checkAsset(Asset asset) throws ThingsboardException {
+        checkNotNull(asset);
+        checkTenantId(asset.getTenantId());
+        if (asset.getCustomerId() != null && !asset.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
+            checkCustomerId(asset.getCustomerId());
+        }
+    }
+
     WidgetsBundle checkWidgetsBundleId(WidgetsBundleId widgetsBundleId, boolean modify) throws ThingsboardException {
         try {
             validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId);
@@ -385,6 +447,16 @@ public abstract class BaseController {
         return plugin;
     }
 
+    protected PluginMetaData checkPlugin(PluginId pluginId) throws ThingsboardException {
+        checkNotNull(pluginId);
+        return checkPlugin(pluginService.findPluginById(pluginId));
+    }
+
+    protected RuleMetaData checkRule(RuleId ruleId) throws ThingsboardException {
+        checkNotNull(ruleId);
+        return checkRule(ruleService.findRuleById(ruleId));
+    }
+
     protected RuleMetaData checkRule(RuleMetaData rule) throws ThingsboardException {
         checkNotNull(rule);
         SecurityUser authUser = getCurrentUser();
@@ -410,7 +482,8 @@ public abstract class BaseController {
         if (request.getHeader("x-forwarded-port") != null) {
             try {
                 serverPort = request.getIntHeader("x-forwarded-port");
-            } catch (NumberFormatException e) {}
+            } catch (NumberFormatException e) {
+            }
         }
 
         String baseUrl = String.format("%s://%s:%d",
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index 96458cf..091eb0e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -15,6 +15,9 @@
  */
 package org.thingsboard.server.controller;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -43,6 +46,28 @@ public class CustomerController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET)
+    @ResponseBody
+    public JsonNode getShortCustomerInfoById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
+        checkParameter("customerId", strCustomerId);
+        try {
+            CustomerId customerId = new CustomerId(toUUID(strCustomerId));
+            Customer customer = checkCustomerId(customerId);
+            ObjectMapper objectMapper = new ObjectMapper();
+            ObjectNode infoObject = objectMapper.createObjectNode();
+            infoObject.put("title", customer.getTitle());
+            boolean isPublic = false;
+            if (customer.getAdditionalInfo() != null && customer.getAdditionalInfo().has("isPublic")) {
+                isPublic = customer.getAdditionalInfo().get("isPublic").asBoolean();
+            }
+            infoObject.put("isPublic", isPublic);
+            return infoObject;
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/customer/{customerId}/title", method = RequestMethod.GET, produces = "application/text")
     @ResponseBody
     public String getCustomerTitleById(@PathVariable("customerId") String strCustomerId) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index d72f025..3812610 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.controller;
 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.Dashboard;
 import org.thingsboard.server.common.data.DashboardInfo;
 import org.thingsboard.server.common.data.id.CustomerId;
@@ -117,6 +118,21 @@ public class DashboardController extends BaseController {
     }
 
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST)
+    @ResponseBody
+    public Dashboard assignDashboardToPublicCustomer(@PathVariable("dashboardId") String strDashboardId) throws ThingsboardException {
+        checkParameter("dashboardId", strDashboardId);
+        try {
+            DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
+            Dashboard dashboard = checkDashboardId(dashboardId);
+            Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId());
+            return checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId()));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
     @RequestMapping(value = "/tenant/dashboards", params = { "limit" }, method = RequestMethod.GET)
     @ResponseBody
     public TextPageData<DashboardInfo> getTenantDashboards(
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 3df1bae..38efcb8 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
 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.Device;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
@@ -114,6 +115,21 @@ public class DeviceController extends BaseController {
         }
     }
 
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST)
+    @ResponseBody
+    public Device assignDeviceToPublicCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
+        checkParameter("deviceId", strDeviceId);
+        try {
+            DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
+            Device device = checkDeviceId(deviceId);
+            Customer publicCustomer = customerService.findOrCreatePublicCustomer(device.getTenantId());
+            return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, publicCustomer.getId()));
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET)
     @ResponseBody
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
new file mode 100644
index 0000000..0c1fd8b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -0,0 +1,194 @@
+/**
+ * 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 org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.exception.ThingsboardException;
+
+import java.util.List;
+
+
+@RestController
+@RequestMapping("/api")
+public class EntityRelationController extends BaseController {
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/relation", method = RequestMethod.POST)
+    @ResponseStatus(value = HttpStatus.OK)
+    public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException {
+        try {
+            checkNotNull(relation);
+            checkEntityId(relation.getFrom());
+            checkEntityId(relation.getTo());
+            relationService.saveRelation(relation).get();
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('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("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
+        checkParameter("fromId", strFromId);
+        checkParameter("fromType", strFromType);
+        checkParameter("relationType", strRelationType);
+        checkParameter("toId", strToId);
+        checkParameter("toType", strToType);
+        EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
+        EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
+        checkEntityId(fromId);
+        checkEntityId(toId);
+        try {
+            Boolean found = relationService.deleteRelation(fromId, toId, strRelationType).get();
+            if (!found) {
+                throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
+            }
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"})
+    @ResponseStatus(value = HttpStatus.OK)
+    public void deleteRelations(@RequestParam("entityId") String strId,
+                                @RequestParam("entityType") String strType) throws ThingsboardException {
+        checkParameter("entityId", strId);
+        checkParameter("entityType", strType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId);
+        checkEntityId(entityId);
+        try {
+            relationService.deleteEntityRelations(entityId).get();
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('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("toId") String strToId, @RequestParam("toType") String strToType) throws ThingsboardException {
+        try {
+            checkParameter("fromId", strFromId);
+            checkParameter("fromType", strFromType);
+            checkParameter("relationType", strRelationType);
+            checkParameter("toId", strToId);
+            checkParameter("toType", strToType);
+            EntityId fromId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
+            EntityId toId = EntityIdFactory.getByTypeAndId(strToType, strToId);
+            checkEntityId(fromId);
+            checkEntityId(toId);
+            Boolean found = relationService.checkRelation(fromId, toId, strRelationType).get();
+            if (!found) {
+                throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
+            }
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('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 {
+        checkParameter("fromId", strFromId);
+        checkParameter("fromType", strFromType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
+        checkEntityId(entityId);
+        try {
+            return checkNotNull(relationService.findByFrom(entityId).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('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 {
+        checkParameter("fromId", strFromId);
+        checkParameter("fromType", strFromType);
+        checkParameter("relationType", strRelationType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strFromType, strFromId);
+        checkEntityId(entityId);
+        try {
+            return checkNotNull(relationService.findByFromAndType(entityId, strRelationType).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('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 {
+        checkParameter("toId", strToId);
+        checkParameter("toType", strToType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
+        checkEntityId(entityId);
+        try {
+            return checkNotNull(relationService.findByTo(entityId).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('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 {
+        checkParameter("toId", strToId);
+        checkParameter("toType", strToType);
+        checkParameter("relationType", strRelationType);
+        EntityId entityId = EntityIdFactory.getByTypeAndId(strToType, strToId);
+        checkEntityId(entityId);
+        try {
+            return checkNotNull(relationService.findByToAndType(entityId, strRelationType).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/relations", method = RequestMethod.POST)
+    @ResponseBody
+    public List<EntityRelation> findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException {
+        checkNotNull(query);
+        checkNotNull(query.getParameters());
+        checkNotNull(query.getFilters());
+        checkEntityId(query.getParameters().getEntityId());
+        try {
+            return checkNotNull(relationService.findByQuery(query).get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
index 1bf6aa9..dc35324 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -18,7 +18,6 @@ package org.thingsboard.server.controller;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
-import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.Event;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.page.TimePageData;
@@ -59,7 +58,7 @@ public class EventController extends BaseController {
                         ThingsboardErrorCode.PERMISSION_DENIED);
             }
             TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
-            return checkNotNull(eventService.findEvents(tenantId, getEntityId(strEntityType, strEntityId), eventType, pageLink));
+            return checkNotNull(eventService.findEvents(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), eventType, pageLink));
         } catch (Exception e) {
             throw handleException(e);
         }
@@ -88,29 +87,10 @@ public class EventController extends BaseController {
                         ThingsboardErrorCode.PERMISSION_DENIED);
             }
             TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset);
-            return checkNotNull(eventService.findEvents(tenantId, getEntityId(strEntityType, strEntityId), pageLink));
+            return checkNotNull(eventService.findEvents(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), pageLink));
         } catch (Exception e) {
             throw handleException(e);
         }
     }
 
-
-    private EntityId getEntityId(String strEntityType, String strEntityId) throws ThingsboardException {
-        EntityId entityId;
-        EntityType entityType = EntityType.valueOf(strEntityType);
-        switch (entityType) {
-            case RULE:
-                entityId = new RuleId(toUUID(strEntityId));
-                break;
-            case PLUGIN:
-                entityId = new PluginId(toUUID(strEntityId));
-                break;
-            case DEVICE:
-                entityId = new DeviceId(toUUID(strEntityId));
-                break;
-            default:
-                throw new ThingsboardException("EntityType ['" + entityType + "'] is incorrect!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
-        }
-        return entityId;
-    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
index 089ade8..c0efbfc 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
@@ -15,11 +15,13 @@
  */
 package org.thingsboard.server.service.cluster.routing;
 
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.service.cluster.discovery.ServerInstance;
 
 import java.util.Optional;
+import java.util.UUID;
 
 /**
  * @author Andrew Shvayka
@@ -28,6 +30,8 @@ public interface ClusterRoutingService {
 
     ServerAddress getCurrentServer();
 
-    Optional<ServerAddress> resolve(UUIDBased entityId);
+    Optional<ServerAddress> resolveByUuid(UUID uuid);
+
+    Optional<ServerAddress> resolveById(EntityId entityId);
 
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
index b57e15e..c29833b 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
@@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.util.Assert;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
@@ -31,6 +32,7 @@ import org.thingsboard.server.utils.MiscUtils;
 
 import javax.annotation.PostConstruct;
 import java.util.Optional;
+import java.util.UUID;
 import java.util.concurrent.ConcurrentNavigableMap;
 import java.util.concurrent.ConcurrentSkipListMap;
 
@@ -77,13 +79,18 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D
     }
 
     @Override
-    public Optional<ServerAddress> resolve(UUIDBased entityId) {
-        Assert.notNull(entityId);
+    public Optional<ServerAddress> resolveById(EntityId entityId) {
+        return resolveByUuid(entityId.getId());
+    }
+
+    @Override
+    public Optional<ServerAddress> resolveByUuid(UUID uuid) {
+        Assert.notNull(uuid);
         if (circle.isEmpty()) {
             return Optional.empty();
         }
-        Long hash = hashFunction.newHasher().putLong(entityId.getId().getMostSignificantBits())
-                .putLong(entityId.getId().getLeastSignificantBits()).hash().asLong();
+        Long hash = hashFunction.newHasher().putLong(uuid.getMostSignificantBits())
+                .putLong(uuid.getLeastSignificantBits()).hash().asLong();
         if (!circle.containsKey(hash)) {
             ConcurrentNavigableMap<Long, ServerInstance> tailMap =
                     circle.tailMap(hash);
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
index 5ba84bf..811f39f 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java
@@ -16,32 +16,40 @@
 package org.thingsboard.server.service.security.auth.jwt;
 
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.authentication.AuthenticationProvider;
-import org.springframework.security.authentication.DisabledException;
-import org.springframework.security.authentication.InsufficientAuthenticationException;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.*;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Component;
 import org.springframework.util.Assert;
+import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken;
 import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
 import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
 
+import java.util.UUID;
+
 @Component
 public class RefreshTokenAuthenticationProvider implements AuthenticationProvider {
 
     private final JwtTokenFactory tokenFactory;
     private final UserService userService;
+    private final CustomerService customerService;
 
     @Autowired
-    public RefreshTokenAuthenticationProvider(final UserService userService, final JwtTokenFactory tokenFactory) {
+    public RefreshTokenAuthenticationProvider(final UserService userService, final CustomerService customerService, final JwtTokenFactory tokenFactory) {
         this.userService = userService;
+        this.customerService = customerService;
         this.tokenFactory = tokenFactory;
     }
 
@@ -50,8 +58,18 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
         Assert.notNull(authentication, "No authentication data provided");
         RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
         SecurityUser unsafeUser = tokenFactory.parseRefreshToken(rawAccessToken);
+        UserPrincipal principal = unsafeUser.getUserPrincipal();
+        SecurityUser securityUser;
+        if (principal.getType() == UserPrincipal.Type.USER_NAME) {
+            securityUser = authenticateByUserId(unsafeUser.getId());
+        } else {
+            securityUser = authenticateByPublicId(principal.getValue());
+        }
+        return new RefreshAuthenticationToken(securityUser);
+    }
 
-        User user = userService.findUserById(unsafeUser.getId());
+    private SecurityUser authenticateByUserId(UserId userId) {
+        User user = userService.findUserById(userId);
         if (user == null) {
             throw new UsernameNotFoundException("User not found by refresh token");
         }
@@ -67,9 +85,44 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide
 
         if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
 
-        SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled());
+        UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail());
 
-        return new RefreshAuthenticationToken(securityUser);
+        SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
+
+        return securityUser;
+    }
+
+    private SecurityUser authenticateByPublicId(String publicId) {
+        CustomerId customerId;
+        try {
+            customerId = new CustomerId(UUID.fromString(publicId));
+        } catch (Exception e) {
+            throw new BadCredentialsException("Refresh token is not valid");
+        }
+        Customer publicCustomer = customerService.findCustomerById(customerId);
+        if (publicCustomer == null) {
+            throw new UsernameNotFoundException("Public entity not found by refresh token");
+        }
+        boolean isPublic = false;
+        if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) {
+            isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean();
+        }
+        if (!isPublic) {
+            throw new BadCredentialsException("Refresh token is not valid");
+        }
+        User user = new User(new UserId(UUIDBased.EMPTY));
+        user.setTenantId(publicCustomer.getTenantId());
+        user.setCustomerId(publicCustomer.getId());
+        user.setEmail(publicId);
+        user.setAuthority(Authority.CUSTOMER_USER);
+        user.setFirstName("Public");
+        user.setLastName("Public");
+
+        UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, publicId);
+
+        SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
+
+        return securityUser;
     }
 
     @Override
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java
new file mode 100644
index 0000000..54ef093
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/PublicLoginRequest.java
@@ -0,0 +1,34 @@
+/**
+ * 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.service.security.auth.rest;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PublicLoginRequest {
+
+    private String publicId;
+
+    @JsonCreator
+    public PublicLoginRequest(@JsonProperty("publicId") String publicId) {
+        this.publicId = publicId;
+   }
+
+    public String getPublicId() {
+        return publicId;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
index 686bb46..af10674 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
@@ -23,20 +23,31 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Component;
 import org.springframework.util.Assert;
+import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
+import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
+
+import java.util.UUID;
 
 @Component
 public class RestAuthenticationProvider implements AuthenticationProvider {
 
     private final BCryptPasswordEncoder encoder;
     private final UserService userService;
+    private final CustomerService customerService;
 
     @Autowired
-    public RestAuthenticationProvider(final UserService userService, final BCryptPasswordEncoder encoder) {
+    public RestAuthenticationProvider(final UserService userService, final CustomerService customerService, final BCryptPasswordEncoder encoder) {
         this.userService = userService;
+        this.customerService = customerService;
         this.encoder = encoder;
     }
 
@@ -44,9 +55,23 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
         Assert.notNull(authentication, "No authentication data provided");
 
-        String username = (String) authentication.getPrincipal();
-        String password = (String) authentication.getCredentials();
+        Object principal = authentication.getPrincipal();
+        if (!(principal instanceof UserPrincipal)) {
+            throw new BadCredentialsException("Authentication Failed. Bad user principal.");
+        }
 
+        UserPrincipal userPrincipal =  (UserPrincipal) principal;
+        if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
+            String username = userPrincipal.getValue();
+            String password = (String) authentication.getCredentials();
+            return authenticateByUsernameAndPassword(userPrincipal, username, password);
+        } else {
+            String publicId = userPrincipal.getValue();
+            return authenticateByPublicId(userPrincipal, publicId);
+        }
+    }
+
+    private Authentication authenticateByUsernameAndPassword(UserPrincipal userPrincipal, String username, String password) {
         User user = userService.findUserByEmail(username);
         if (user == null) {
             throw new UsernameNotFoundException("User not found: " + username);
@@ -67,7 +92,38 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
 
         if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned");
 
-        SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled());
+        SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
+
+        return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
+    }
+
+    private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
+        CustomerId customerId;
+        try {
+            customerId = new CustomerId(UUID.fromString(publicId));
+        } catch (Exception e) {
+            throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
+        }
+        Customer publicCustomer = customerService.findCustomerById(customerId);
+        if (publicCustomer == null) {
+            throw new UsernameNotFoundException("Public entity not found: " + publicId);
+        }
+        boolean isPublic = false;
+        if (publicCustomer.getAdditionalInfo() != null && publicCustomer.getAdditionalInfo().has("isPublic")) {
+            isPublic = publicCustomer.getAdditionalInfo().get("isPublic").asBoolean();
+        }
+        if (!isPublic) {
+            throw new BadCredentialsException("Authentication Failed. Public Id is not valid.");
+        }
+        User user = new User(new UserId(UUIDBased.EMPTY));
+        user.setTenantId(publicCustomer.getTenantId());
+        user.setCustomerId(publicCustomer.getId());
+        user.setEmail(publicId);
+        user.setAuthority(Authority.CUSTOMER_USER);
+        user.setFirstName("Public");
+        user.setLastName("Public");
+
+        SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
 
         return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
     }
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
index d191905..c32b1f2 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestLoginProcessingFilter.java
@@ -29,6 +29,7 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
+import org.thingsboard.server.service.security.model.UserPrincipal;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
@@ -73,7 +74,9 @@ public class RestLoginProcessingFilter extends AbstractAuthenticationProcessingF
             throw new AuthenticationServiceException("Username or Password not provided");
         }
 
-        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
+        UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, loginRequest.getUsername());
+
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, loginRequest.getPassword());
 
         return this.getAuthenticationManager().authenticate(token);
     }
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
new file mode 100644
index 0000000..3a3b7cb
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestPublicLoginProcessingFilter.java
@@ -0,0 +1,96 @@
+/**
+ * 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.service.security.auth.rest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
+import org.thingsboard.server.service.security.model.UserPrincipal;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class RestPublicLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
+    private static Logger logger = LoggerFactory.getLogger(RestPublicLoginProcessingFilter.class);
+
+    private final AuthenticationSuccessHandler successHandler;
+    private final AuthenticationFailureHandler failureHandler;
+
+    private final ObjectMapper objectMapper;
+
+    public RestPublicLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
+                                     AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
+        super(defaultProcessUrl);
+        this.successHandler = successHandler;
+        this.failureHandler = failureHandler;
+        this.objectMapper = mapper;
+    }
+
+    @Override
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+            throws AuthenticationException, IOException, ServletException {
+        if (!HttpMethod.POST.name().equals(request.getMethod())) {
+            if(logger.isDebugEnabled()) {
+                logger.debug("Authentication method not supported. Request method: " + request.getMethod());
+            }
+            throw new AuthMethodNotSupportedException("Authentication method not supported");
+        }
+
+        PublicLoginRequest loginRequest;
+        try {
+            loginRequest = objectMapper.readValue(request.getReader(), PublicLoginRequest.class);
+        } catch (Exception e) {
+            throw new AuthenticationServiceException("Invalid public login request payload");
+        }
+
+        if (StringUtils.isBlank(loginRequest.getPublicId())) {
+            throw new AuthenticationServiceException("Public Id is not provided");
+        }
+
+        UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, loginRequest.getPublicId());
+
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "");
+
+        return this.getAuthenticationManager().authenticate(token);
+    }
+
+    @Override
+    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+                                            Authentication authResult) throws IOException, ServletException {
+        successHandler.onAuthenticationSuccess(request, response, authResult);
+    }
+
+    @Override
+    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
+                                              AuthenticationException failed) throws IOException, ServletException {
+        SecurityContextHolder.clearContext();
+        failureHandler.onAuthenticationFailure(request, response, failed);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
index 2da3a97..0839695 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
@@ -30,6 +30,7 @@ public class SecurityUser extends User {
 
     private Collection<GrantedAuthority> authorities;
     private boolean enabled;
+    private UserPrincipal userPrincipal;
 
     public SecurityUser() {
         super();
@@ -39,9 +40,10 @@ public class SecurityUser extends User {
         super(id);
     }
 
-    public SecurityUser(User user, boolean enabled) {
+    public SecurityUser(User user, boolean enabled, UserPrincipal userPrincipal) {
         super(user);
         this.enabled = enabled;
+        this.userPrincipal = userPrincipal;
     }
 
     public Collection<? extends GrantedAuthority> getAuthorities() {
@@ -57,8 +59,16 @@ public class SecurityUser extends User {
         return enabled;
     }
 
+    public UserPrincipal getUserPrincipal() {
+        return userPrincipal;
+    }
+
     public void setEnabled(boolean enabled) {
         this.enabled = enabled;
     }
 
+    public void setUserPrincipal(UserPrincipal userPrincipal) {
+        this.userPrincipal = userPrincipal;
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
index 8ade253..20de6d8 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.config.JwtSettings;
 import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.security.model.UserPrincipal;
 
 import java.util.Arrays;
 import java.util.List;
@@ -43,6 +44,7 @@ public class JwtTokenFactory {
     private static final String FIRST_NAME = "firstName";
     private static final String LAST_NAME = "lastName";
     private static final String ENABLED = "enabled";
+    private static final String IS_PUBLIC = "isPublic";
     private static final String TENANT_ID = "tenantId";
     private static final String CUSTOMER_ID = "customerId";
 
@@ -63,12 +65,15 @@ public class JwtTokenFactory {
         if (securityUser.getAuthority() == null)
             throw new IllegalArgumentException("User doesn't have any privileges");
 
-        Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
+        UserPrincipal principal = securityUser.getUserPrincipal();
+        String subject = principal.getValue();
+        Claims claims = Jwts.claims().setSubject(subject);
         claims.put(SCOPES, securityUser.getAuthorities().stream().map(s -> s.getAuthority()).collect(Collectors.toList()));
         claims.put(USER_ID, securityUser.getId().getId().toString());
         claims.put(FIRST_NAME, securityUser.getFirstName());
         claims.put(LAST_NAME, securityUser.getLastName());
         claims.put(ENABLED, securityUser.isEnabled());
+        claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
         if (securityUser.getTenantId() != null) {
             claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
         }
@@ -104,6 +109,9 @@ public class JwtTokenFactory {
         securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
         securityUser.setLastName(claims.get(LAST_NAME, String.class));
         securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
+        boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
+        UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
+        securityUser.setUserPrincipal(principal);
         String tenantId = claims.get(TENANT_ID, String.class);
         if (tenantId != null) {
             securityUser.setTenantId(new TenantId(UUID.fromString(tenantId)));
@@ -123,9 +131,11 @@ public class JwtTokenFactory {
 
         DateTime currentTime = new DateTime();
 
-        Claims claims = Jwts.claims().setSubject(securityUser.getEmail());
+        UserPrincipal principal = securityUser.getUserPrincipal();
+        Claims claims = Jwts.claims().setSubject(principal.getValue());
         claims.put(SCOPES, Arrays.asList(Authority.REFRESH_TOKEN.name()));
         claims.put(USER_ID, securityUser.getId().getId().toString());
+        claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
 
         String token = Jwts.builder()
                 .setClaims(claims)
@@ -150,8 +160,10 @@ public class JwtTokenFactory {
         if (!scopes.get(0).equals(Authority.REFRESH_TOKEN.name())) {
             throw new IllegalArgumentException("Invalid Refresh Token scope");
         }
+        boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
+        UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
         SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
-        securityUser.setEmail(subject);
+        securityUser.setUserPrincipal(principal);
         return securityUser;
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java
new file mode 100644
index 0000000..6f23783
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/UserPrincipal.java
@@ -0,0 +1,42 @@
+/**
+ * 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.service.security.model;
+
+public class UserPrincipal {
+
+    private final Type type;
+    private final String value;
+
+    public UserPrincipal(Type type, String value) {
+        this.type = type;
+        this.value = value;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public enum Type {
+        USER_NAME,
+        PUBLIC_ID
+    }
+
+}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index c626725..3bacbfa 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -60,8 +60,8 @@ plugins:
 
 # JWT Token parameters
 security.jwt:
-  tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins)
-  refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour)
+  tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000000}" # Number of seconds (15 mins)
+  refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:36000000}" # Seconds (1 hour)
   tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
   tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
 
@@ -189,11 +189,33 @@ cache:
 updates:
   # Enable/disable updates checking.
   enabled: "${UPDATES_ENABLED:true}"
-
+  
+  # spring CORS configuration
+spring.mvc.cors:
+   mappings:
+     # Intercept path
+      "/api/auth/**":
+         #Comma-separated list of origins to allow. '*' allows all origins. When not set,CORS support is disabled.
+         allowed-origins: "*"
+         #Comma-separated list of methods to allow. '*' allows all methods.
+         allowed-methods: "POST,GET,OPTIONS"
+         #Comma-separated list of headers to allow in a request. '*' allows all headers.
+         allowed-headers: "*"
+         #How long, in seconds, the response from a pre-flight request can be cached by clients.
+         max-age: "1800"
+         #Set whether credentials are supported. When not set, credentials are not supported.
+         allow-credentials: "true"
+      "/api/v1/**":
+         allowed-origins: "*"
+         allowed-methods: "*"
+         allowed-headers: "*"
+         max-age: "1800"
+         allow-credentials: "true"
+         
 # SQL DAO Configuration
 sql:
   enabled: "${SQL_ENABLED:false}"
   datasource:
     url: "${SQL_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}"
     username: "${SQL_DATASOURCE_USERNAME:postgres}"
-    password: "${SQL_DATASOURCE_PASSWORD:postgres}"
+    password: "${SQL_DATASOURCE_PASSWORD:postgres}"         
diff --git a/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
index 2940a62..857e083 100644
--- a/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
+++ b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
@@ -145,7 +145,7 @@ public class DefaultActorServiceTest {
         ReflectionTestUtils.setField(actorContext, "eventService", eventService);
 
 
-        when(routingService.resolve(any())).thenReturn(Optional.empty());
+        when(routingService.resolveById((EntityId) any())).thenReturn(Optional.empty());
 
         when(discoveryService.getCurrentServer()).thenReturn(serverInstance);
 
@@ -239,7 +239,7 @@ public class DefaultActorServiceTest {
         List<TsKvEntry> expected = new ArrayList<>();
         expected.add(new BasicTsKvEntry(ts, entry1));
         expected.add(new BasicTsKvEntry(ts, entry2));
-        verify(tsService, Mockito.timeout(5000)).save(DataConstants.DEVICE, deviceId, expected);
+        verify(tsService, Mockito.timeout(5000)).save(deviceId, expected);
     }
 
 }
diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
index 896c549..b1076e0 100644
--- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
@@ -288,7 +288,7 @@ public class CustomerControllerTest extends AbstractControllerTest {
         for (int i=0;i<143;i++) {
             Customer customer = new Customer();
             customer.setTenantId(tenantId);
-            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
             String title = title1+suffix;
             title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
             customer.setTitle(title);
@@ -299,7 +299,7 @@ public class CustomerControllerTest extends AbstractControllerTest {
         for (int i=0;i<175;i++) {
             Customer customer = new Customer();
             customer.setTenantId(tenantId);
-            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
             String title = title2+suffix;
             title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
             customer.setTitle(title);
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 ca4abfe..5d72076 100644
--- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
@@ -149,7 +149,7 @@ public class TenantControllerTest extends AbstractControllerTest {
         List<Tenant> tenantsTitle1 = new ArrayList<>();
         for (int i=0;i<134;i++) {
             Tenant tenant = new Tenant();
-            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
             String title = title1+suffix;
             title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
             tenant.setTitle(title);
@@ -159,7 +159,7 @@ public class TenantControllerTest extends AbstractControllerTest {
         List<Tenant> tenantsTitle2 = new ArrayList<>();
         for (int i=0;i<127;i++) {
             Tenant tenant = new Tenant();
-            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
             String title = title2+suffix;
             title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
             tenant.setTitle(title);
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
new file mode 100644
index 0000000..6691670
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -0,0 +1,55 @@
+/**
+ * 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.alarm;
+
+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.id.AssetId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+@Data
+@Builder
+@AllArgsConstructor
+public class Alarm extends BaseData<AlarmId> {
+
+    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);
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java
new file mode 100644
index 0000000..ad0fd5b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmId.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.alarm;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+import java.util.UUID;
+
+public class AlarmId extends UUIDBased implements EntityId {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public AlarmId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    public static AlarmId fromString(String alarmId) {
+        return new AlarmId(UUID.fromString(alarmId));
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.ALARM;
+    }
+}
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
new file mode 100644
index 0000000..2dc0189
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
@@ -0,0 +1,34 @@
+/**
+ * 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.alarm;
+
+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
+public class AlarmQuery {
+
+    private TenantId tenantId;
+    private EntityId affectedEntityId;
+    private TimePageLink pageLink;
+    private AlarmStatus status;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java
new file mode 100644
index 0000000..ed73027
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java
@@ -0,0 +1,25 @@
+/**
+ * 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.alarm;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public enum AlarmSeverity {
+
+    CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE;
+
+}
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
new file mode 100644
index 0000000..0f1b346
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java
@@ -0,0 +1,33 @@
+/**
+ * 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.alarm;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+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
new file mode 100644
index 0000000..2e20b77
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -0,0 +1,166 @@
+/**
+ * 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 com.fasterxml.jackson.databind.JsonNode;
+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> {
+
+    private static final long serialVersionUID = 2807343040519543363L;
+
+    private TenantId tenantId;
+    private CustomerId customerId;
+    private String name;
+    private String type;
+    private JsonNode additionalInfo;
+
+    public Asset() {
+        super();
+    }
+
+    public Asset(AssetId id) {
+        super(id);
+    }
+
+    public Asset(Asset asset) {
+        super(asset);
+        this.tenantId = asset.getTenantId();
+        this.customerId = asset.getCustomerId();
+        this.name = asset.getName();
+        this.type = asset.getType();
+        this.additionalInfo = asset.getAdditionalInfo();
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+    public void setCustomerId(CustomerId customerId) {
+        this.customerId = customerId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+    
+    @Override
+    public String getSearchText() {
+        return name;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+        result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((type == null) ? 0 : type.hashCode());
+        result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Asset other = (Asset) obj;
+        if (additionalInfo == null) {
+            if (other.additionalInfo != null)
+                return false;
+        } else if (!additionalInfo.equals(other.additionalInfo))
+            return false;
+        if (customerId == null) {
+            if (other.customerId != null)
+                return false;
+        } else if (!customerId.equals(other.customerId))
+            return false;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (type == null) {
+            if (other.type != null)
+                return false;
+        } else if (!type.equals(other.type))
+            return false;
+        if (tenantId == null) {
+            if (other.tenantId != null)
+                return false;
+        } else if (!tenantId.equals(other.tenantId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Asset [tenantId=");
+        builder.append(tenantId);
+        builder.append(", customerId=");
+        builder.append(customerId);
+        builder.append(", name=");
+        builder.append(name);
+        builder.append(", type=");
+        builder.append(type);
+        builder.append(", additionalInfo=");
+        builder.append(additionalInfo);
+        builder.append(", createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
index 8dac8f5..ebf42f2 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
@@ -19,5 +19,5 @@ package org.thingsboard.server.common.data;
  * @author Andrew Shvayka
  */
 public enum EntityType {
-    TENANT, DEVICE, CUSTOMER, RULE, PLUGIN
+    TENANT, CUSTOMER, USER, RULE, PLUGIN, DASHBOARD, ASSET, DEVICE, ALARM
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
new file mode 100644
index 0000000..049116e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java
@@ -0,0 +1,43 @@
+/**
+ * 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.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+public class AssetId extends UUIDBased implements EntityId {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public AssetId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    public static AssetId fromString(String assetId) {
+        return new AssetId(UUID.fromString(assetId));
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.ASSET;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
index 632d5c2..a9a3441 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
@@ -18,12 +18,24 @@ package org.thingsboard.server.common.data.id;
 import java.util.UUID;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
 
-public class DashboardId extends UUIDBased {
+public class DashboardId extends UUIDBased implements EntityId {
 
     @JsonCreator
-    public DashboardId(@JsonProperty("id") UUID id){
+    public DashboardId(@JsonProperty("id") UUID id) {
         super(id);
     }
+
+    public static DashboardId fromString(String dashboardId) {
+        return new DashboardId(UUID.fromString(dashboardId));
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.DASHBOARD;
+    }
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
index f0caec5..17d69ac 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
@@ -16,6 +16,8 @@
 package org.thingsboard.server.common.data.id;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import org.thingsboard.server.common.data.EntityType;
 
 import java.util.UUID;
@@ -23,6 +25,9 @@ import java.util.UUID;
 /**
  * @author Andrew Shvayka
  */
+
+@JsonDeserialize(using = EntityIdDeserializer.class)
+@JsonSerialize(using = EntityIdSerializer.class)
 public interface EntityId {
 
     UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080");
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java
new file mode 100644
index 0000000..f9f34c4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdDeserializer.java
@@ -0,0 +1,43 @@
+/**
+ * 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.id;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.ObjectCodec;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.io.IOException;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public class EntityIdDeserializer extends JsonDeserializer<EntityId> {
+
+    @Override
+    public EntityId deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException, JsonProcessingException {
+        ObjectCodec oc = jsonParser.getCodec();
+        ObjectNode node = oc.readTree(jsonParser);
+        if (node.has("entityType") && node.has("id")) {
+            return EntityIdFactory.getByTypeAndId(node.get("entityType").asText(), node.get("id").asText());
+        } else {
+            throw new IOException("Missing entityType or id!");
+        }
+    }
+
+}
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
new file mode 100644
index 0000000..21093ce
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -0,0 +1,56 @@
+/**
+ * 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.id;
+
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+/**
+ * Created by ashvayka on 25.04.17.
+ */
+public class EntityIdFactory {
+
+    public static EntityId getByTypeAndId(String type, String uuid) {
+        return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid));
+    }
+
+    public static EntityId getByTypeAndUuid(String type, UUID uuid) {
+        return getByTypeAndUuid(EntityType.valueOf(type), uuid);
+    }
+
+    public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) {
+        switch (type) {
+            case TENANT:
+                return new TenantId(uuid);
+            case CUSTOMER:
+                return new CustomerId(uuid);
+            case USER:
+                return new UserId(uuid);
+            case RULE:
+                return new RuleId(uuid);
+            case PLUGIN:
+                return new PluginId(uuid);
+            case DASHBOARD:
+                return new DashboardId(uuid);
+            case DEVICE:
+                return new DeviceId(uuid);
+            case ASSET:
+                return new AssetId(uuid);
+        }
+        throw new IllegalArgumentException("EntityType " + type + " is not supported!");
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdSerializer.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdSerializer.java
new file mode 100644
index 0000000..18ffaea
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdSerializer.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import java.io.IOException;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public class EntityIdSerializer extends JsonSerializer<EntityId> {
+
+    @Override
+    public void serialize(EntityId value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
+        gen.writeStartObject();
+        gen.writeStringField("entityType", value.getEntityType().name());
+        gen.writeStringField("id", value.getId().toString());
+        gen.writeEndObject();
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
index 51b1140..31b6642 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
@@ -18,12 +18,25 @@ package org.thingsboard.server.common.data.id;
 import java.util.UUID;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
 
-public class UserId extends UUIDBased {
+public class UserId extends UUIDBased implements EntityId {
 
     @JsonCreator
-	public UserId(@JsonProperty("id") UUID id){
-		super(id);
-	}
+    public UserId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    public static UserId fromString(String userId) {
+        return new UserId(UUID.fromString(userId));
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.USER;
+    }
+
 }
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
new file mode 100644
index 0000000..8fcf269
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java
@@ -0,0 +1,103 @@
+/**
+ * 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;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.id.EntityId;
+
+import java.util.Objects;
+
+public class EntityRelation {
+
+    private static final long serialVersionUID = 2807343040519543363L;
+
+    public static final String CONTAINS_TYPE = "Contains";
+    public static final String MANAGES_TYPE = "Manages";
+
+    private EntityId from;
+    private EntityId to;
+    private String type;
+    private JsonNode additionalInfo;
+
+    public EntityRelation() {
+        super();
+    }
+
+    public EntityRelation(EntityId from, EntityId to, String type) {
+        this(from, to, type, null);
+    }
+
+    public EntityRelation(EntityId from, EntityId to, String type, JsonNode additionalInfo) {
+        this.from = from;
+        this.to = to;
+        this.type = type;
+        this.additionalInfo = additionalInfo;
+    }
+
+    public EntityRelation(EntityRelation device) {
+        this.from = device.getFrom();
+        this.to = device.getTo();
+        this.type = device.getType();
+        this.additionalInfo = device.getAdditionalInfo();
+    }
+
+    public EntityId getFrom() {
+        return from;
+    }
+
+    public void setFrom(EntityId from) {
+        this.from = from;
+    }
+
+    public EntityId getTo() {
+        return to;
+    }
+
+    public void setTo(EntityId to) {
+        this.to = to;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+
+    @Override
+    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);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(from, to, type);
+    }
+}
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
new file mode 100644
index 0000000..8f32b4c
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
@@ -0,0 +1,38 @@
+/**
+ * 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.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.alarm.Alarm;
+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.UUID;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public interface AlarmDao extends Dao<Alarm> {
+
+    ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
+
+    ListenableFuture<Alarm> findAlarmByIdAsync(UUID key);
+
+    Alarm save(Alarm alarm);
+
+}
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..dcb01ba
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDaoImpl.java
@@ -0,0 +1,77 @@
+/**
+ * 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.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.CassandraAbstractModelDao;
+import org.thingsboard.server.dao.model.AlarmEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+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 CassandraAbstractModelDao<AlarmEntity, Alarm> implements AlarmDao {
+
+    @Override
+    protected Class<AlarmEntity> getColumnFamilyClass() {
+        return AlarmEntity.class;
+    }
+
+    @Override
+    protected String getColumnFamilyName() {
+        return ALARM_COLUMN_FAMILY_NAME;
+    }
+
+    @Override
+    public Alarm save(Alarm alarm) {
+        log.debug("Save asset [{}] ", alarm);
+        return super.save(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 findOneByStatementAsync(query);
+    }
+
+    @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 findOneByStatementAsync(query);
+    }
+}
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
new file mode 100644
index 0000000..6508db2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.alarm;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.Device;
+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.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.page.TimePageData;
+
+import java.util.Optional;
+
+/**
+ * Created by ashvayka on 11.05.17.
+ */
+public interface AlarmService {
+
+    Alarm createOrUpdateAlarm(Alarm alarm);
+
+    ListenableFuture<Boolean> updateAlarm(Alarm alarm);
+
+    ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTs);
+
+    ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long ackTs);
+
+    ListenableFuture<Alarm> findAlarmById(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..6d484ba
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
@@ -0,0 +1,252 @@
+/**
+ * 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.Tenant;
+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.dao.entity.BaseEntityService;
+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.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 java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
+@Service
+@Slf4j
+public class BaseAlarmService extends BaseEntityService implements AlarmService {
+
+    private static final String ALARM_RELATION_PREFIX = "ALARM_";
+    private static final String ALARM_RELATION = "ALARM_ANY";
+
+    @Autowired
+    private AlarmDao alarmDao;
+
+    @Autowired
+    private TenantDao tenantDao;
+
+    @Autowired
+    private RelationService relationService;
+
+    @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());
+            }
+            Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get();
+            if (existing == null || existing.getStatus().isCleared()) {
+                log.debug("New Alarm : {}", alarm);
+                Alarm saved = alarmDao.save(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));
+                    createRelation(new EntityRelation(parentId, saved.getId(), ALARM_RELATION_PREFIX + saved.getStatus().name()));
+                }
+                return saved;
+            } else {
+                log.debug("Alarm before merge: {}", alarm);
+                alarm = merge(existing, alarm);
+                log.debug("Alarm after merge: {}", alarm);
+                return alarmDao.save(alarm);
+            }
+        } catch (ExecutionException | InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public ListenableFuture<Boolean> updateAlarm(Alarm update) {
+        alarmDataValidator.validate(update);
+        return getAndUpdate(update.getId(), new Function<Alarm, Boolean>() {
+            @Nullable
+            @Override
+            public Boolean apply(@Nullable Alarm alarm) {
+                if (alarm == null) {
+                    return false;
+                } else {
+                    AlarmStatus oldStatus = alarm.getStatus();
+                    AlarmStatus newStatus = update.getStatus();
+                    alarmDao.save(merge(alarm, update));
+                    if (oldStatus != newStatus) {
+                        updateRelations(alarm, oldStatus, newStatus);
+                    }
+                    return true;
+                }
+            }
+        });
+    }
+
+    @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(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(alarm);
+                    updateRelations(alarm, oldStatus, newStatus);
+                    return true;
+                }
+            }
+        });
+    }
+
+    @Override
+    public ListenableFuture<Alarm> findAlarmById(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) {
+        return null;
+    }
+
+    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()));
+                createRelation(new EntityRelation(parentId, alarm.getId(), ALARM_RELATION_PREFIX + newStatus.name()));
+            }
+        } catch (ExecutionException | InterruptedException e) {
+            log.warn("[{}] Failed to update relations. Old status: [{}], New status: [{}]", alarm.getId(), oldStatus, newStatus);
+            throw new RuntimeException(e);
+        }
+    }
+
+    private ListenableFuture<Boolean> getAndUpdate(AlarmId alarmId, Function<Alarm, Boolean> function) {
+        validateId(alarmId, "Alarm id should be specified!");
+        ListenableFuture<Alarm> entity = alarmDao.findAlarmByIdAsync(alarmId.getId());
+        return Futures.transform(entity, function);
+    }
+
+    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 {
+                        Tenant 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
new file mode 100644
index 0000000..220cbcb
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDao.java
@@ -0,0 +1,87 @@
+/**
+ * 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.asset;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.Dao;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * The Interface AssetDao.
+ *
+ */
+public interface AssetDao extends Dao<Asset> {
+
+    /**
+     * Save or update asset object
+     *
+     * @param asset the asset object
+     * @return saved asset object
+     */
+    Asset save(Asset asset);
+
+    /**
+     * Find assets by tenantId and page link.
+     *
+     * @param tenantId the tenantId
+     * @param pageLink the page link
+     * @return the list of asset objects
+     */
+    List<Asset> findAssetsByTenantId(UUID tenantId, TextPageLink pageLink);
+
+    /**
+     * Find assets by tenantId and assets Ids.
+     *
+     * @param tenantId the tenantId
+     * @param assetIds the asset Ids
+     * @return the list of asset objects
+     */
+    ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List<UUID> assetIds);
+
+    /**
+     * Find assets by tenantId, customerId and page link.
+     *
+     * @param tenantId the tenantId
+     * @param customerId the customerId
+     * @param pageLink the page link
+     * @return the list of asset objects
+     */
+    List<Asset> findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
+
+    /**
+     * Find assets by tenantId, customerId and assets Ids.
+     *
+     * @param tenantId the tenantId
+     * @param customerId the customerId
+     * @param assetIds the asset Ids
+     * @return the list of asset objects
+     */
+    ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> assetIds);
+
+    /**
+     * Find assets by tenantId and asset name.
+     *
+     * @param tenantId the tenantId
+     * @param name the asset name
+     * @return the optional asset object
+     */
+    Optional<Asset> findAssetsByTenantIdAndName(UUID tenantId, String name);
+}
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
new file mode 100644
index 0000000..2f2cf13
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetDaoImpl.java
@@ -0,0 +1,106 @@
+/**
+ * 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.asset;
+
+import com.datastax.driver.core.querybuilder.Select;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.dao.CassandraAbstractSearchTextDao;
+import org.thingsboard.server.dao.DaoUtil;
+import org.thingsboard.server.dao.model.AssetEntity;
+
+import java.util.*;
+
+import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Component
+@Slf4j
+public class AssetDaoImpl extends CassandraAbstractSearchTextDao<AssetEntity, Asset> implements AssetDao {
+
+    @Override
+    protected Class<AssetEntity> getColumnFamilyClass() {
+        return AssetEntity.class;
+    }
+
+    @Override
+    protected String getColumnFamilyName() {
+        return ASSET_COLUMN_FAMILY_NAME;
+    }
+
+    @Override
+    public Asset save(Asset asset) {
+        log.debug("Save asset [{}] ", asset);
+        return save(asset);
+    }
+
+    @Override
+    public List<Asset> findAssetsByTenantId(UUID tenantId, TextPageLink pageLink) {
+        log.debug("Try to find assets by tenantId [{}] and pageLink [{}]", tenantId, pageLink);
+        List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Collections.singletonList(eq(ASSET_TENANT_ID_PROPERTY, tenantId)), pageLink);
+
+        log.trace("Found assets [{}] by tenantId [{}] and pageLink [{}]", assetEntities, tenantId, pageLink);
+        return DaoUtil.convertDataList(assetEntities);
+    }
+
+    @Override
+    public ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(UUID tenantId, List<UUID> assetIds) {
+        log.debug("Try to find assets by tenantId [{}] and asset Ids [{}]", tenantId, assetIds);
+        Select select = select().from(getColumnFamilyName());
+        Select.Where query = select.where();
+        query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId));
+        query.and(in(ID_PROPERTY, assetIds));
+        return findListByStatementAsync(query);
+    }
+
+    @Override
+    public List<Asset> findAssetsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
+        log.debug("Try to find assets by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
+        List<AssetEntity> assetEntities = findPageWithTextSearch(ASSET_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME,
+                Arrays.asList(eq(ASSET_CUSTOMER_ID_PROPERTY, customerId),
+                        eq(ASSET_TENANT_ID_PROPERTY, tenantId)),
+                pageLink);
+
+        log.trace("Found assets [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", assetEntities, tenantId, customerId, pageLink);
+        return DaoUtil.convertDataList(assetEntities);
+    }
+
+    @Override
+    public ListenableFuture<List<Asset>> 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());
+        Select.Where query = select.where();
+        query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId));
+        query.and(eq(ASSET_CUSTOMER_ID_PROPERTY, customerId));
+        query.and(in(ID_PROPERTY, assetIds));
+        return findListByStatementAsync(query);
+    }
+
+    @Override
+    public Optional<Asset> findAssetsByTenantIdAndName(UUID tenantId, String assetName) {
+        Select select = select().from(ASSET_BY_TENANT_AND_NAME_VIEW_NAME);
+        Select.Where query = select.where();
+        query.and(eq(ASSET_TENANT_ID_PROPERTY, tenantId));
+        query.and(eq(ASSET_NAME_PROPERTY, assetName));
+        AssetEntity assetEntity = (AssetEntity) findOneByStatement(query);
+        return Optional.ofNullable(DaoUtil.getData(assetEntity));
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java
new file mode 100644
index 0000000..f3c69b2
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetSearchQuery.java
@@ -0,0 +1,50 @@
+/**
+ * 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.asset;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntityTypeFilter;
+
+import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 03.05.17.
+ */
+@Data
+public class AssetSearchQuery {
+
+    private RelationsSearchParameters parameters;
+    @Nullable
+    private String relationType;
+    @Nullable
+    private List<String> assetTypes;
+
+    public EntityRelationsQuery toEntitySearchQuery() {
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        query.setParameters(parameters);
+        query.setFilters(
+                Collections.singletonList(new EntityTypeFilter(relationType == null ? EntityRelation.CONTAINS_TYPE : relationType,
+                        Collections.singletonList(EntityType.ASSET))));
+        return query;
+    }
+}
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
new file mode 100644
index 0000000..7a61bd8
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
@@ -0,0 +1,59 @@
+/**
+ * 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.asset;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface AssetService {
+
+    Asset findAssetById(AssetId assetId);
+
+    ListenableFuture<Asset> findAssetByIdAsync(AssetId assetId);
+
+    Optional<Asset> findAssetByTenantIdAndName(TenantId tenantId, String name);
+
+    Asset saveAsset(Asset device);
+
+    Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId);
+
+    Asset unassignAssetFromCustomer(AssetId assetId);
+
+    void deleteAsset(AssetId assetId);
+
+    TextPageData<Asset> findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink);
+
+    ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds);
+
+    void deleteAssetsByTenantId(TenantId tenantId);
+
+    TextPageData<Asset> findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, 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);
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetTypeFilter.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetTypeFilter.java
new file mode 100644
index 0000000..221e4df
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetTypeFilter.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.asset;
+
+import lombok.Data;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 02.05.17.
+ */
+@Data
+public class AssetTypeFilter {
+    @Nullable
+    private String relationType;
+    @Nullable
+    private List<String> assetTypes;
+}
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
new file mode 100644
index 0000000..addf17b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
@@ -0,0 +1,282 @@
+/**
+ * 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.asset;
+
+
+import com.google.common.base.Function;
+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.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.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.exception.DataValidationException;
+import org.thingsboard.server.dao.model.AssetEntity;
+import org.thingsboard.server.dao.model.nosql.CustomerEntity;
+import org.thingsboard.server.dao.model.nosql.TenantEntity;
+import org.thingsboard.server.dao.relation.EntityRelationsQuery;
+import org.thingsboard.server.dao.relation.EntitySearchDirection;
+import org.thingsboard.server.dao.service.DataValidator;
+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.stream.Collectors;
+
+import static org.thingsboard.server.dao.DaoUtil.*;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.thingsboard.server.dao.service.Validator.*;
+
+@Service
+@Slf4j
+public class BaseAssetService extends BaseEntityService implements AssetService {
+
+    @Autowired
+    private AssetDao assetDao;
+
+    @Autowired
+    private TenantDao tenantDao;
+
+    @Autowired
+    private CustomerDao customerDao;
+
+    @Override
+    public Asset findAssetById(AssetId assetId) {
+        log.trace("Executing findAssetById [{}]", assetId);
+        validateId(assetId, "Incorrect assetId " + assetId);
+        return assetDao.findById(assetId.getId());
+    }
+
+    @Override
+    public ListenableFuture<Asset> findAssetByIdAsync(AssetId assetId) {
+        log.trace("Executing findAssetById [{}]", assetId);
+        validateId(assetId, "Incorrect assetId " + assetId);
+        return assetDao.findByIdAsync(assetId.getId());
+    }
+
+    @Override
+    public Optional<Asset> findAssetByTenantIdAndName(TenantId tenantId, String name) {
+        log.trace("Executing findAssetByTenantIdAndName [{}][{}]", tenantId, name);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        return assetDao.findAssetsByTenantIdAndName(tenantId.getId(), name);
+    }
+
+    @Override
+    public Asset saveAsset(Asset asset) {
+        log.trace("Executing saveAsset [{}]", asset);
+        assetValidator.validate(asset);
+        return assetDao.save(asset);
+    }
+
+    @Override
+    public Asset assignAssetToCustomer(AssetId assetId, CustomerId customerId) {
+        Asset asset = findAssetById(assetId);
+        asset.setCustomerId(customerId);
+        return saveAsset(asset);
+    }
+
+    @Override
+    public Asset unassignAssetFromCustomer(AssetId assetId) {
+        Asset asset = findAssetById(assetId);
+        asset.setCustomerId(null);
+        return saveAsset(asset);
+    }
+
+    @Override
+    public void deleteAsset(AssetId assetId) {
+        log.trace("Executing deleteAsset [{}]", assetId);
+        validateId(assetId, "Incorrect assetId " + assetId);
+        deleteEntityRelations(assetId);
+        assetDao.removeById(assetId.getId());
+    }
+
+    @Override
+    public TextPageData<Asset> findAssetsByTenantId(TenantId tenantId, TextPageLink pageLink) {
+        log.trace("Executing findAssetsByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<Asset> assets = assetDao.findAssetsByTenantId(tenantId.getId(), pageLink);
+        return new TextPageData<>(assets, pageLink);
+    }
+
+    @Override
+    public ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds) {
+        log.trace("Executing findAssetsByTenantIdAndIdsAsync, tenantId [{}], assetIds [{}]", tenantId, assetIds);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateIds(assetIds, "Incorrect assetIds " + assetIds);
+        return assetDao.findAssetsByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(assetIds));
+    }
+
+    @Override
+    public void deleteAssetsByTenantId(TenantId tenantId) {
+        log.trace("Executing deleteAssetsByTenantId, tenantId [{}]", tenantId);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        tenantAssetsRemover.removeEntitites(tenantId);
+    }
+
+    @Override
+    public TextPageData<Asset> findAssetsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) {
+        log.trace("Executing findAssetsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        validatePageLink(pageLink, "Incorrect page link " + pageLink);
+        List<Asset> assets = assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink);
+        return new TextPageData<Asset>(assets, pageLink);
+    }
+
+    @Override
+    public ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<AssetId> assetIds) {
+        log.trace("Executing findAssetsByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], assetIds [{}]", tenantId, customerId, assetIds);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        validateIds(assetIds, "Incorrect assetIds " + assetIds);
+        return assetDao.findAssetsByTenantIdCustomerIdAndIdsAsync(tenantId.getId(), customerId.getId(), toUUIDs(assetIds));
+    }
+
+    @Override
+    public void unassignCustomerAssets(TenantId tenantId, CustomerId customerId) {
+        log.trace("Executing unassignCustomerAssets, tenantId [{}], customerId [{}]", tenantId, customerId);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        new CustomerAssetsUnassigner(tenantId).removeEntitites(customerId);
+    }
+
+    @Override
+    public ListenableFuture<List<Asset>> findAssetsByQuery(AssetSearchQuery query) {
+        ListenableFuture<List<EntityRelation>> relations = relationService.findByQuery(query.toEntitySearchQuery());
+        ListenableFuture<List<Asset>> assets = Futures.transform(relations, (AsyncFunction<List<EntityRelation>, List<Asset>>) relations1 -> {
+            EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection();
+            List<ListenableFuture<Asset>> futures = new ArrayList<>();
+            for (EntityRelation relation : relations1) {
+                EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
+                if (entityId.getEntityType() == EntityType.ASSET) {
+                    futures.add(findAssetByIdAsync(new AssetId(entityId.getId())));
+                }
+            }
+            return Futures.successfulAsList(futures);
+        });
+
+        assets = Futures.transform(assets, new Function<List<Asset>, List<Asset>>() {
+            @Nullable
+            @Override
+            public List<Asset> apply(@Nullable List<Asset> assetList) {
+                return assetList.stream().filter(asset -> query.getAssetTypes().contains(asset.getType())).collect(Collectors.toList());
+            }
+        });
+
+        return assets;
+    }
+
+    private DataValidator<Asset> assetValidator =
+            new DataValidator<Asset>() {
+
+                @Override
+                protected void validateCreate(Asset asset) {
+                    assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
+                            d -> {
+                                throw new DataValidationException("Asset with such name already exists!");
+                            }
+                    );
+                }
+
+                @Override
+                protected void validateUpdate(Asset asset) {
+                    assetDao.findAssetsByTenantIdAndName(asset.getTenantId().getId(), asset.getName()).ifPresent(
+                            d -> {
+                                if (!d.getId().equals(asset.getUuidId())) {
+                                    throw new DataValidationException("Asset with such name already exists!");
+                                }
+                            }
+                    );
+                }
+
+                @Override
+                protected void validateDataImpl(Asset asset) {
+                    if (StringUtils.isEmpty(asset.getName())) {
+                        throw new DataValidationException("Asset name should be specified!");
+                    }
+                    if (asset.getTenantId() == null) {
+                        throw new DataValidationException("Asset should be assigned to tenant!");
+                    } else {
+                        Tenant tenant = tenantDao.findById(asset.getTenantId().getId());
+                        if (tenant == null) {
+                            throw new DataValidationException("Asset is referencing to non-existent tenant!");
+                        }
+                    }
+                    if (asset.getCustomerId() == null) {
+                        asset.setCustomerId(new CustomerId(NULL_UUID));
+                    } else if (!asset.getCustomerId().getId().equals(NULL_UUID)) {
+                        Customer customer = customerDao.findById(asset.getCustomerId().getId());
+                        if (customer == null) {
+                            throw new DataValidationException("Can't assign asset to non-existent customer!");
+                        }
+                        if (!customer.getTenantId().equals(asset.getTenantId().getId())) {
+                            throw new DataValidationException("Can't assign asset to customer from different tenant!");
+                        }
+                    }
+                }
+            };
+
+    private PaginatedRemover<TenantId, Asset> tenantAssetsRemover =
+            new PaginatedRemover<TenantId, Asset>() {
+
+                @Override
+                protected List<Asset> findEntities(TenantId id, TextPageLink pageLink) {
+                    return assetDao.findAssetsByTenantId(id.getId(), pageLink);
+                }
+
+                @Override
+                protected void removeEntity(Asset entity) {
+                    deleteAsset(new AssetId(entity.getId().getId()));
+                }
+            };
+
+    class CustomerAssetsUnassigner extends PaginatedRemover<CustomerId, Asset> {
+
+        private TenantId tenantId;
+
+        CustomerAssetsUnassigner(TenantId tenantId) {
+            this.tenantId = tenantId;
+        }
+
+        @Override
+        protected List<Asset> findEntities(CustomerId id, TextPageLink pageLink) {
+            return assetDao.findAssetsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink);
+        }
+
+        @Override
+        protected void removeEntity(Asset entity) {
+            unassignAssetFromCustomer(new AssetId(entity.getId().getId()));
+        }
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java
index 99c82a0..b35ed37 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/CassandraAbstractModelDao.java
@@ -187,4 +187,14 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
         List<E> entities = findListByStatement(QueryBuilder.select().all().from(getColumnFamilyName()).setConsistencyLevel(cluster.getDefaultReadConsistencyLevel()));
         return DaoUtil.convertDataList(entities);
     }
+
+    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/customer/CassandraCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
index 1bb682b..92ca9c4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CassandraCustomerDao.java
@@ -15,6 +15,13 @@
  */
 package org.thingsboard.server.dao.customer;
 
+import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TITLE_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_TENANT_ID_PROPERTY;
+
+import java.util.Optional;
+import com.datastax.driver.core.querybuilder.Select;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Customer;
@@ -53,4 +60,15 @@ public class CassandraCustomerDao extends CassandraAbstractSearchTextDao<Custome
         return DaoUtil.convertDataList(customerEntities);
     }
 
+    @Override
+    public Optional<Customer> findCustomersByTenantIdAndTitle(UUID tenantId, String title) {
+        Select select = select().from(CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME);
+        Select.Where query = select.where();
+        query.and(eq(CUSTOMER_TENANT_ID_PROPERTY, tenantId));
+        query.and(eq(CUSTOMER_TITLE_PROPERTY, title));
+        CustomerEntity customerEntity = findOneByStatement(query);
+        Customer customer = DaoUtil.getData(customerEntity);
+        return Optional.ofNullable(customer);
+    }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
index e26fc98..3c8f66d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerDao.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.customer;
 
+import java.util.Optional;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.Dao;
@@ -43,5 +44,14 @@ public interface CustomerDao extends Dao<Customer> {
      * @return the list of customer objects
      */
     List<Customer> findCustomersByTenantId(UUID tenantId, TextPageLink pageLink);
+
+    /**
+     * Find customers by tenantId and customer title.
+     *
+     * @param tenantId the tenantId
+     * @param title the customer title
+     * @return the optional customer object
+     */
+    Optional<Customer> findCustomersByTenantIdAndTitle(UUID tenantId, String title);
     
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
index 566d718..4600d9f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.customer;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -24,13 +25,17 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 public interface CustomerService {
 
     Customer findCustomerById(CustomerId customerId);
-    
+
+    ListenableFuture<Customer> findCustomerByIdAsync(CustomerId customerId);
+
     Customer saveCustomer(Customer customer);
     
     void deleteCustomer(CustomerId customerId);
-    
+
+    Customer findOrCreatePublicCustomer(TenantId tenantId);
+
     TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink);
     
     void deleteCustomersByTenantId(TenantId tenantId);
-    
+
 }
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 3e9e472..ba5c9a9 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
@@ -15,30 +15,42 @@
  */
 package org.thingsboard.server.dao.customer;
 
+import static org.thingsboard.server.dao.service.Validator.validateId;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 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.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.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
+import org.thingsboard.server.dao.model.AssetEntity;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
 import org.thingsboard.server.dao.service.Validator;
 import org.thingsboard.server.dao.tenant.TenantDao;
 import org.thingsboard.server.dao.user.UserService;
-
-import java.util.List;
 @Service
 @Slf4j
-public class CustomerServiceImpl implements CustomerService {
+public class CustomerServiceImpl extends BaseEntityService implements CustomerService {
+
+    private static final String PUBLIC_CUSTOMER_TITLE = "Public";
 
     @Autowired
     private CustomerDao customerDao;
@@ -63,6 +75,13 @@ public class CustomerServiceImpl implements CustomerService {
     }
 
     @Override
+    public ListenableFuture<Customer> findCustomerByIdAsync(CustomerId customerId) {
+        log.trace("Executing findCustomerByIdAsync [{}]", customerId);
+        validateId(customerId, "Incorrect customerId " + customerId);
+        return customerDao.findByIdAsync(customerId.getId());
+    }
+
+    @Override
     public Customer saveCustomer(Customer customer) {
         log.trace("Executing saveCustomer [{}]", customer);
         customerValidator.validate(customer);
@@ -72,24 +91,45 @@ public class CustomerServiceImpl implements CustomerService {
     @Override
     public void deleteCustomer(CustomerId customerId) {
         log.trace("Executing deleteCustomer [{}]", customerId);
-        Validator.validateId(customerId, "Incorrect tenantId " + customerId);
+        Validator.validateId(customerId, "Incorrect customerId " + customerId);
         Customer customer = findCustomerById(customerId);
         if (customer == null) {
             throw new IncorrectParameterException("Unable to delete non-existent customer.");
         }
         dashboardService.unassignCustomerDashboards(customer.getTenantId(), customerId);
         deviceService.unassignCustomerDevices(customer.getTenantId(), customerId);
-        userService.deleteCustomerUsers(customer.getTenantId(), customerId);               
+        userService.deleteCustomerUsers(customer.getTenantId(), customerId);
+        deleteEntityRelations(customerId);
         customerDao.removeById(customerId.getId());
     }
 
     @Override
+    public Customer findOrCreatePublicCustomer(TenantId tenantId) {
+        log.trace("Executing findOrCreatePublicCustomer, tenantId [{}]", tenantId);
+        Validator.validateId(tenantId, "Incorrect customerId " + tenantId);
+        Optional<Customer> publicCustomerOpt = customerDao.findCustomersByTenantIdAndTitle(tenantId.getId(), PUBLIC_CUSTOMER_TITLE);
+        if (publicCustomerOpt.isPresent()) {
+            return publicCustomerOpt.get();
+        } else {
+            Customer publicCustomer = new Customer();
+            publicCustomer.setTenantId(tenantId);
+            publicCustomer.setTitle(PUBLIC_CUSTOMER_TITLE);
+            try {
+                publicCustomer.setAdditionalInfo(new ObjectMapper().readValue("{ \"isPublic\": true }", JsonNode.class));
+            } catch (IOException e) {
+                throw new IncorrectParameterException("Unable to create public customer.", e);
+            }
+            return customerDao.save(publicCustomer);
+        }
+    }
+
+    @Override
     public TextPageData<Customer> findCustomersByTenantId(TenantId tenantId, TextPageLink pageLink) {
         log.trace("Executing findCustomersByTenantId, tenantId [{}], pageLink [{}]", tenantId, pageLink);
         Validator.validateId(tenantId, "Incorrect tenantId " + tenantId);
         Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink);
         List<Customer> customers = customerDao.findCustomersByTenantId(tenantId.getId(), pageLink);
-        return new TextPageData<Customer>(customers, pageLink);
+        return new TextPageData<>(customers, pageLink);
     }
 
     @Override
@@ -101,11 +141,35 @@ public class CustomerServiceImpl implements CustomerService {
     
     private DataValidator<Customer> customerValidator =
             new DataValidator<Customer>() {
+
+                @Override
+                protected void validateCreate(Customer customer) {
+                    customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
+                            c -> {
+                                throw new DataValidationException("Customer with such title already exists!");
+                            }
+                    );
+                }
+
+                @Override
+                protected void validateUpdate(Customer customer) {
+                    customerDao.findCustomersByTenantIdAndTitle(customer.getTenantId().getId(), customer.getTitle()).ifPresent(
+                            c -> {
+                                if (!c.getId().equals(customer.getUuidId())) {
+                                    throw new DataValidationException("Customer with such title already exists!");
+                                }
+                            }
+                    );
+                }
+
                 @Override
                 protected void validateDataImpl(Customer customer) {
                     if (StringUtils.isEmpty(customer.getTitle())) {
                         throw new DataValidationException("Customer title should be specified!");
                     }
+                    if (customer.getTitle().equals(PUBLIC_CUSTOMER_TITLE)) {
+                        throw new DataValidationException("'Public' title for customer is system reserved!");
+                    }
                     if (!StringUtils.isEmpty(customer.getEmail())) {
                         validateEmail(customer.getEmail());
                     }
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 fca73c4..bdca536 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
@@ -29,6 +29,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.exception.DataValidationException;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.service.DataValidator;
@@ -40,7 +41,7 @@ import java.util.List;
 
 @Service
 @Slf4j
-public class DashboardServiceImpl implements DashboardService {
+public class DashboardServiceImpl extends BaseEntityService implements DashboardService {
 
     @Autowired
     private DashboardDao dashboardDao;
@@ -86,6 +87,7 @@ public class DashboardServiceImpl implements DashboardService {
     public void deleteDashboard(DashboardId dashboardId) {
         log.trace("Executing deleteDashboard [{}]", dashboardId);
         Validator.validateId(dashboardId, "Incorrect dashboardId " + dashboardId);
+        deleteEntityRelations(dashboardId);
         dashboardDao.removeById(dashboardId.getId());
     }
 
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 a5707ce..819f3ae 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
@@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 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.exception.DataValidationException;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
@@ -48,7 +49,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
 
 @Service
 @Slf4j
-public class DeviceServiceImpl implements DeviceService {
+public class DeviceServiceImpl extends BaseEntityService implements DeviceService {
 
     @Autowired
     private DeviceDao deviceDao;
@@ -125,6 +126,7 @@ public class DeviceServiceImpl implements DeviceService {
         if (deviceCredentials != null) {
             deviceCredentialsService.deleteDeviceCredentials(deviceCredentials);
         }
+        deleteEntityRelations(deviceId);
         deviceDao.removeById(deviceId.getId());
     }
 
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
new file mode 100644
index 0000000..3c1c528
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.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;
+
+/**
+ * Created by ashvayka on 04.05.17.
+ */
+@Slf4j
+public class BaseEntityService {
+
+    @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/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
new file mode 100644
index 0000000..0444d11
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/AssetEntity.java
@@ -0,0 +1,235 @@
+/**
+ * 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.Column;
+import com.datastax.driver.mapping.annotations.PartitionKey;
+import com.datastax.driver.mapping.annotations.Table;
+import com.datastax.driver.mapping.annotations.Transient;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.model.type.JsonCodec;
+
+import java.util.UUID;
+
+import static org.thingsboard.server.dao.model.ModelConstants.*;
+
+@Table(name = ASSET_COLUMN_FAMILY_NAME)
+public final class AssetEntity implements SearchTextEntity<Asset> {
+
+    @Transient
+    private static final long serialVersionUID = -1265181166886910152L;
+
+    @PartitionKey(value = 0)
+    @Column(name = ID_PROPERTY)
+    private UUID id;
+
+    @PartitionKey(value = 1)
+    @Column(name = ASSET_TENANT_ID_PROPERTY)
+    private UUID tenantId;
+
+    @PartitionKey(value = 2)
+    @Column(name = ASSET_CUSTOMER_ID_PROPERTY)
+    private UUID customerId;
+
+    @Column(name = ASSET_NAME_PROPERTY)
+    private String name;
+
+    @Column(name = ASSET_TYPE_PROPERTY)
+    private String type;
+
+    @Column(name = SEARCH_TEXT_PROPERTY)
+    private String searchText;
+
+    @Column(name = ASSET_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
+    private JsonNode additionalInfo;
+
+    public AssetEntity() {
+        super();
+    }
+
+    public AssetEntity(Asset asset) {
+        if (asset.getId() != null) {
+            this.id = asset.getId().getId();
+        }
+        if (asset.getTenantId() != null) {
+            this.tenantId = asset.getTenantId().getId();
+        }
+        if (asset.getCustomerId() != null) {
+            this.customerId = asset.getCustomerId().getId();
+        }
+        this.name = asset.getName();
+        this.type = asset.getType();
+        this.additionalInfo = asset.getAdditionalInfo();
+    }
+
+    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 getCustomerId() {
+        return customerId;
+    }
+
+    public void setCustomerId(UUID customerId) {
+        this.customerId = customerId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+
+    @Override
+    public String getSearchTextSource() {
+        return name;
+    }
+
+    @Override
+    public void setSearchText(String searchText) {
+        this.searchText = searchText;
+    }
+
+    public String getSearchText() {
+        return searchText;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+        result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+        result = prime * result + ((id == null) ? 0 : id.hashCode());
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((type == null) ? 0 : type.hashCode());
+        result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        AssetEntity other = (AssetEntity) obj;
+        if (additionalInfo == null) {
+            if (other.additionalInfo != null)
+                return false;
+        } else if (!additionalInfo.equals(other.additionalInfo))
+            return false;
+        if (customerId == null) {
+            if (other.customerId != null)
+                return false;
+        } else if (!customerId.equals(other.customerId))
+            return false;
+        if (id == null) {
+            if (other.id != null)
+                return false;
+        } else if (!id.equals(other.id))
+            return false;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (type == null) {
+            if (other.type != null)
+                return false;
+        } else if (!type.equals(other.type))
+            return false;
+        if (tenantId == null) {
+            if (other.tenantId != null)
+                return false;
+        } else if (!tenantId.equals(other.tenantId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("AssetEntity [id=");
+        builder.append(id);
+        builder.append(", tenantId=");
+        builder.append(tenantId);
+        builder.append(", customerId=");
+        builder.append(customerId);
+        builder.append(", name=");
+        builder.append(name);
+        builder.append(", type=");
+        builder.append(type);
+        builder.append(", additionalInfo=");
+        builder.append(additionalInfo);
+        builder.append("]");
+        return builder.toString();
+    }
+
+    @Override
+    public Asset toData() {
+        Asset asset = new Asset(new AssetId(id));
+        asset.setCreatedTime(UUIDs.unixTimestamp(id));
+        if (tenantId != null) {
+            asset.setTenantId(new TenantId(tenantId));
+        }
+        if (customerId != null) {
+            asset.setCustomerId(new CustomerId(customerId));
+        }
+        asset.setName(name);
+        asset.setType(type);
+        asset.setAdditionalInfo(additionalInfo);
+        return asset;
+    }
+
+}
\ No newline at end of file
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 b721780..ff2a37a 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
@@ -112,6 +112,7 @@ public class ModelConstants {
     public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
 
     public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text";
+    public static final String CUSTOMER_BY_TENANT_AND_TITLE_VIEW_NAME = "customer_by_tenant_and_title";
 
     /**
      * Cassandra device constants.
@@ -126,6 +127,51 @@ public class ModelConstants {
     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_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
 
+    /**
+     * Cassandra asset constants.
+     */
+    public static final String ASSET_COLUMN_FAMILY_NAME = "asset";
+    public static final String ASSET_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY;
+    public static final String ASSET_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
+    public static final String ASSET_NAME_PROPERTY = "name";
+    public static final String ASSET_TYPE_PROPERTY = "type";
+    public static final String ASSET_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_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "asset_by_customer_and_search_text";
+    public static final String ASSET_BY_TENANT_AND_NAME_VIEW_NAME = "asset_by_tenant_and_name";
+
+    /**
+     * Cassandra alarm constants.
+     */
+    public static final String ALARM_COLUMN_FAMILY_NAME = "alarm";
+    public static final String ALARM_TENANT_ID_PROPERTY = TENANT_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.
+     */
+    public static final String RELATION_COLUMN_FAMILY_NAME = "relation";
+    public static final String RELATION_FROM_ID_PROPERTY = "from_id";
+    public static final String RELATION_FROM_TYPE_PROPERTY = "from_type";
+    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_REVERSE_VIEW_NAME = "reverse_relation";
+
 
     /**
      * Cassandra device_credentials constants.
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
index acd5b8d..8224fb8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/EventEntity.java
@@ -99,23 +99,7 @@ public class EventEntity implements BaseEntity<Event> {
         Event event = new Event(new EventId(id));
         event.setCreatedTime(UUIDs.unixTimestamp(id));
         event.setTenantId(new TenantId(tenantId));
-        switch (entityType) {
-            case TENANT:
-                event.setEntityId(new TenantId(entityId));
-                break;
-            case DEVICE:
-                event.setEntityId(new DeviceId(entityId));
-                break;
-            case CUSTOMER:
-                event.setEntityId(new CustomerId(entityId));
-                break;
-            case RULE:
-                event.setEntityId(new RuleId(entityId));
-                break;
-            case PLUGIN:
-                event.setEntityId(new PluginId(entityId));
-                break;
-        }
+        event.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId));
         event.setBody(body);
         event.setType(eventType);
         event.setUid(eventUid);
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/plugin/BasePluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/BasePluginService.java
index b9b73c6..5784840 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
@@ -15,11 +15,14 @@
  */
 package org.thingsboard.server.dao.plugin;
 
+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.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;
@@ -29,6 +32,7 @@ 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.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.DatabaseException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -43,9 +47,10 @@ import java.util.List;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
+import static org.thingsboard.server.dao.service.Validator.validateId;
 @Service
 @Slf4j
-public class BasePluginService implements PluginService {
+public class BasePluginService extends BaseEntityService implements PluginService {
 
     //TODO: move to a better place.
     public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
@@ -103,6 +108,12 @@ public class BasePluginService implements PluginService {
     }
 
     @Override
+    public ListenableFuture<PluginMetaData> findPluginByIdAsync(PluginId pluginId) {
+        validateId(pluginId, "Incorrect plugin id for search plugin request.");
+        return pluginDao.findByIdAsync(pluginId.getId());
+    }
+
+    @Override
     public PluginMetaData findPluginByApiToken(String apiToken) {
         Validator.validateString(apiToken, "Incorrect plugin apiToken for search request.");
         return pluginDao.findByApiToken(apiToken);
@@ -196,6 +207,7 @@ public class BasePluginService implements PluginService {
     @Override
     public void deletePluginById(PluginId pluginId) {
         Validator.validateId(pluginId, "Incorrect plugin id for delete request.");
+        deleteEntityRelations(pluginId);
         checkRulesAndDelete(pluginId.getId());
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java
index f8173ff..371a46e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/plugin/PluginService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.plugin;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -29,6 +30,8 @@ public interface PluginService {
 
     PluginMetaData findPluginById(PluginId pluginId);
 
+    ListenableFuture<PluginMetaData> findPluginByIdAsync(PluginId pluginId);
+
     PluginMetaData findPluginByApiToken(String apiToken);
 
     TextPageData<PluginMetaData> findSystemPlugins(TextPageLink pageLink);
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
new file mode 100644
index 0000000..d3a886b
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -0,0 +1,279 @@
+/**
+ * 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.relation;
+
+import com.datastax.driver.core.*;
+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.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.dao.CassandraAbstractAsyncDao;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 25.04.17.
+ */
+@Component
+@Slf4j
+public class BaseRelationDao extends CassandraAbstractAsyncDao implements RelationDao {
+
+    private static final String SELECT_COLUMNS = "SELECT " +
+            ModelConstants.RELATION_FROM_ID_PROPERTY + "," +
+            ModelConstants.RELATION_FROM_TYPE_PROPERTY + "," +
+            ModelConstants.RELATION_TO_ID_PROPERTY + "," +
+            ModelConstants.RELATION_TO_TYPE_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 PreparedStatement saveStmt;
+    private PreparedStatement findAllByFromStmt;
+    private PreparedStatement findAllByFromAndTypeStmt;
+    private PreparedStatement findAllByToStmt;
+    private PreparedStatement findAllByToAndTypeStmt;
+    private PreparedStatement checkRelationStmt;
+    private PreparedStatement deleteStmt;
+    private PreparedStatement deleteAllByEntityStmt;
+
+    @PostConstruct
+    public void init() {
+        super.startExecutor();
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from) {
+        BoundStatement stmt = getFindAllByFromStmt().bind().setUUID(0, from.getId()).setString(1, from.getEntityType().name());
+        return executeAsyncRead(from, stmt);
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType) {
+        BoundStatement stmt = getFindAllByFromAndTypeStmt().bind()
+                .setUUID(0, from.getId())
+                .setString(1, from.getEntityType().name())
+                .setString(2, 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());
+        return executeAsyncRead(to, stmt);
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType) {
+        BoundStatement stmt = getFindAllByToAndTypeStmt().bind()
+                .setUUID(0, to.getId())
+                .setString(1, to.getEntityType().name())
+                .setString(2, relationType);
+        return executeAsyncRead(to, stmt);
+    }
+
+    @Override
+    public ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType) {
+        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);
+        return getFuture(executeAsyncRead(stmt), rs -> rs != null ? rs.one() != null : false);
+    }
+
+    @Override
+    public ListenableFuture<Boolean> saveRelation(EntityRelation relation) {
+        BoundStatement stmt = getSaveStmt().bind()
+                .setUUID(0, relation.getFrom().getId())
+                .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);
+        ResultSetFuture future = executeAsyncWrite(stmt);
+        return getBooleanListenableFuture(future);
+    }
+
+    @Override
+    public ListenableFuture<Boolean> deleteRelation(EntityRelation relation) {
+        return deleteRelation(relation.getFrom(), relation.getTo(), relation.getType());
+    }
+
+    @Override
+    public ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType) {
+        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);
+        ResultSetFuture future = executeAsyncWrite(stmt);
+        return getBooleanListenableFuture(future);
+    }
+
+    @Override
+    public ListenableFuture<Boolean> deleteOutboundRelations(EntityId entity) {
+        BoundStatement stmt = getDeleteAllByEntityStmt().bind()
+                .setUUID(0, entity.getId())
+                .setString(1, entity.getEntityType().name());
+        ResultSetFuture future = executeAsyncWrite(stmt);
+        return getBooleanListenableFuture(future);
+    }
+
+    private PreparedStatement getSaveStmt() {
+        if (saveStmt == null) {
+            saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
+                    "(" + ModelConstants.RELATION_FROM_ID_PROPERTY +
+                    "," + ModelConstants.RELATION_FROM_TYPE_PROPERTY +
+                    "," + ModelConstants.RELATION_TO_ID_PROPERTY +
+                    "," + ModelConstants.RELATION_TO_TYPE_PROPERTY +
+                    "," + ModelConstants.RELATION_TYPE_PROPERTY +
+                    "," + ModelConstants.ADDITIONAL_INFO_PROPERTY + ")" +
+                    " VALUES(?, ?, ?, ?, ?, ?)");
+        }
+        return saveStmt;
+    }
+
+    private PreparedStatement getDeleteStmt() {
+        if (deleteStmt == null) {
+            deleteStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
+                    WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
+                    AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" +
+                    AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" +
+                    AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + " = ?" +
+                    AND + ModelConstants.RELATION_TYPE_PROPERTY + " = ?");
+        }
+        return deleteStmt;
+    }
+
+    private PreparedStatement getDeleteAllByEntityStmt() {
+        if (deleteAllByEntityStmt == null) {
+            deleteAllByEntityStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
+                    WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
+                    AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?");
+        }
+        return deleteAllByEntityStmt;
+    }
+
+    private PreparedStatement getFindAllByFromStmt() {
+        if (findAllByFromStmt == null) {
+            findAllByFromStmt = getSession().prepare(SELECT_COLUMNS + " " +
+                    FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
+                    WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ? ");
+        }
+        return findAllByFromStmt;
+    }
+
+    private PreparedStatement getFindAllByFromAndTypeStmt() {
+        if (findAllByFromAndTypeStmt == null) {
+            findAllByFromAndTypeStmt = 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_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 + " = ? ");
+        }
+        return findAllByToStmt;
+    }
+
+    private PreparedStatement getFindAllByToAndTypeStmt() {
+        if (findAllByToAndTypeStmt == null) {
+            findAllByToAndTypeStmt = 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_TYPE_PROPERTY + " = ? ");
+        }
+        return findAllByToAndTypeStmt;
+    }
+
+    private PreparedStatement getCheckRelationStmt() {
+        if (checkRelationStmt == null) {
+            checkRelationStmt = 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_TO_ID_PROPERTY + " = ? " +
+                    AND + ModelConstants.RELATION_TO_TYPE_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;
+        });
+    }
+
+    private ListenableFuture<Boolean> getBooleanListenableFuture(ResultSetFuture rsFuture) {
+        return getFuture(rsFuture, rs -> rs != null ? rs.wasApplied() : false);
+    }
+
+    private <T> ListenableFuture<T> getFuture(ResultSetFuture future, java.util.function.Function<ResultSet, T> transformer) {
+        return Futures.transform(future, new Function<ResultSet, T>() {
+            @Nullable
+            @Override
+            public T apply(@Nullable ResultSet input) {
+                return transformer.apply(input);
+            }
+        }, readResultsProcessingExecutor);
+    }
+
+}
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
new file mode 100644
index 0000000..998f2ef
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -0,0 +1,261 @@
+/**
+ * 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.relation;
+
+import com.google.common.base.Function;
+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.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.dao.exception.DataValidationException;
+
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by ashvayka on 28.04.17.
+ */
+@Service
+@Slf4j
+public class BaseRelationService implements RelationService {
+
+    @Autowired
+    private RelationDao relationDao;
+
+    @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);
+    }
+
+    @Override
+    public ListenableFuture<Boolean> saveRelation(EntityRelation relation) {
+        log.trace("Executing saveRelation [{}]", relation);
+        validate(relation);
+        return relationDao.saveRelation(relation);
+    }
+
+    @Override
+    public ListenableFuture<Boolean> deleteRelation(EntityRelation relation) {
+        log.trace("Executing deleteRelation [{}]", relation);
+        validate(relation);
+        return relationDao.deleteRelation(relation);
+    }
+
+    @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);
+    }
+
+    @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>>() {
+            @Override
+            public ListenableFuture<List<Boolean>> apply(List<EntityRelation> relations) throws Exception {
+                List<ListenableFuture<Boolean>> results = new ArrayList<>();
+                for (EntityRelation relation : relations) {
+                    results.add(relationDao.deleteRelation(relation));
+                }
+                return Futures.allAsList(results);
+            }
+        });
+
+        ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
+
+        ListenableFuture<Boolean> outboundFuture = relationDao.deleteOutboundRelations(entity);
+
+        return Futures.transform(Futures.allAsList(Arrays.asList(inboundFuture, outboundFuture)), getListToBooleanFunction());
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findByFrom(EntityId from) {
+        log.trace("Executing findByFrom [{}]", from);
+        validate(from);
+        return relationDao.findAllByFrom(from);
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType) {
+        log.trace("Executing findByFromAndType [{}][{}]", from, relationType);
+        validate(from);
+        validateType(relationType);
+        return relationDao.findAllByFromAndType(from, relationType);
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findByTo(EntityId to) {
+        log.trace("Executing findByTo [{}]", to);
+        validate(to);
+        return relationDao.findAllByTo(to);
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType) {
+        log.trace("Executing findByToAndType [{}][{}]", to, relationType);
+        validate(to);
+        validateType(relationType);
+        return relationDao.findAllByToAndType(to, relationType);
+    }
+
+    @Override
+    public ListenableFuture<List<EntityRelation>> findByQuery(EntityRelationsQuery query) {
+        log.trace("Executing findByQuery [{}][{}]", query);
+        RelationsSearchParameters params = query.getParameters();
+        final List<EntityTypeFilter> filters = query.getFilters();
+        if (filters == null || filters.isEmpty()) {
+            log.debug("Filters are not set [{}]", query);
+        }
+
+        int maxLvl = params.getMaxLevel() > 0 ? params.getMaxLevel() : Integer.MAX_VALUE;
+
+        try {
+            ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(params.getEntityId(), params.getDirection(), maxLvl, new ConcurrentHashMap<>());
+            return Futures.transform(relationSet, (Function<Set<EntityRelation>, List<EntityRelation>>) input -> {
+                List<EntityRelation> relations = new ArrayList<>();
+                for (EntityRelation relation : input) {
+                    if (filters == null || filters.isEmpty()) {
+                        relations.add(relation);
+                    } else {
+                        for (EntityTypeFilter filter : filters) {
+                            if (match(filter, relation, params.getDirection())) {
+                                relations.add(relation);
+                                break;
+                            }
+                        }
+                    }
+                }
+                return relations;
+            });
+        } catch (Exception e) {
+            log.warn("Failed to query relations: [{}]", query, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected void validate(EntityRelation relation) {
+        if (relation == null) {
+            throw new DataValidationException("Relation type should be specified!");
+        }
+        validate(relation.getFrom(), relation.getTo(), relation.getType());
+    }
+
+    protected void validate(EntityId from, EntityId to, String type) {
+        validateType(type);
+        if (from == null) {
+            throw new DataValidationException("Relation should contain from entity!");
+        }
+        if (to == null) {
+            throw new DataValidationException("Relation should contain to entity!");
+        }
+    }
+
+    private void validateType(String type) {
+        if (StringUtils.isEmpty(type)) {
+            throw new DataValidationException("Relation type should be specified!");
+        }
+    }
+
+    protected void validate(EntityId entity) {
+        if (entity == null) {
+            throw new DataValidationException("Entity should be specified!");
+        }
+    }
+
+    private Function<List<Boolean>, Boolean> getListToBooleanFunction() {
+        return new Function<List<Boolean>, Boolean>() {
+            @Nullable
+            @Override
+            public Boolean apply(@Nullable List<Boolean> results) {
+                for (Boolean result : results) {
+                    if (result == null || !result) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        };
+    }
+
+    private boolean match(EntityTypeFilter filter, EntityRelation relation, EntitySearchDirection direction) {
+        if (StringUtils.isEmpty(filter.getRelationType()) || filter.getRelationType().equals(relation.getType())) {
+            if (filter.getEntityTypes() == null || filter.getEntityTypes().isEmpty()) {
+                return true;
+            } else {
+                EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom();
+                return filter.getEntityTypes().contains(entityId.getEntityType());
+            }
+        } else {
+            return false;
+        }
+    }
+
+    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());
+        }
+        lvl--;
+        //TODO: try to remove this blocking operation
+        Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
+        Set<EntityId> childrenIds = new HashSet<>();
+        for (EntityRelation childRelation : children) {
+            log.info("Found Relation: {}", childRelation);
+            EntityId childId;
+            if (direction == EntitySearchDirection.FROM) {
+                childId = childRelation.getTo();
+            } else {
+                childId = childRelation.getFrom();
+            }
+            if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
+                log.info("Adding Relation: {}", childId);
+                if (childrenIds.add(childId)) {
+                    log.info("Added Relation: {}", childId);
+                }
+            }
+        }
+        List<ListenableFuture<Set<EntityRelation>>> futures = new ArrayList<>();
+        for (EntityId entityId : childrenIds) {
+            futures.add(findRelationsRecursively(entityId, direction, lvl, uniqueMap));
+        }
+        //TODO: try to remove this blocking operation
+        List<Set<EntityRelation>> relations = Futures.successfulAsList(futures).get();
+        relations.forEach(r -> r.forEach(d -> children.add(d)));
+        return Futures.immediateFuture(children);
+    }
+
+    private ListenableFuture<List<EntityRelation>> findRelations(final EntityId rootId, final EntitySearchDirection direction) {
+        ListenableFuture<List<EntityRelation>> relations;
+        if (direction == EntitySearchDirection.FROM) {
+            relations = findByFrom(rootId);
+        } else {
+            relations = findByTo(rootId);
+        }
+        return relations;
+    }
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationsQuery.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationsQuery.java
new file mode 100644
index 0000000..b19e9e5
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityRelationsQuery.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.dao.relation;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * Created by ashvayka on 02.05.17.
+ */
+@Data
+public class EntityRelationsQuery {
+
+    private RelationsSearchParameters parameters;
+    private List<EntityTypeFilter> filters;
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntitySearchDirection.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntitySearchDirection.java
new file mode 100644
index 0000000..64715b1
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntitySearchDirection.java
@@ -0,0 +1,25 @@
+/**
+ * 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.relation;
+
+/**
+ * Created by ashvayka on 02.05.17.
+ */
+public enum EntitySearchDirection {
+
+    FROM, TO;
+
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/EntityTypeFilter.java b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityTypeFilter.java
new file mode 100644
index 0000000..9618ecf
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/EntityTypeFilter.java
@@ -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.
+ */
+package org.thingsboard.server.dao.relation;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.EntityType;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Created by ashvayka on 02.05.17.
+ */
+@Data
+@AllArgsConstructor
+public class EntityTypeFilter {
+    @Nullable
+    private String relationType;
+    @Nullable
+    private List<EntityType> entityTypes;
+}
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
new file mode 100644
index 0000000..df47259
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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 java.util.List;
+
+/**
+ * Created by ashvayka on 25.04.17.
+ */
+public interface RelationDao {
+
+    ListenableFuture<List<EntityRelation>> findAllByFrom(EntityId from);
+
+    ListenableFuture<List<EntityRelation>> findAllByFromAndType(EntityId from, String relationType);
+
+    ListenableFuture<List<EntityRelation>> findAllByTo(EntityId to);
+
+    ListenableFuture<List<EntityRelation>> findAllByToAndType(EntityId to, String relationType);
+
+    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+
+    ListenableFuture<Boolean> saveRelation(EntityRelation relation);
+
+    ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
+
+    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+
+    ListenableFuture<Boolean> deleteOutboundRelations(EntityId entity);
+
+}
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
new file mode 100644
index 0000000..e3e2a1f
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationService.java
@@ -0,0 +1,52 @@
+/**
+ * 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.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 java.util.List;
+
+/**
+ * Created by ashvayka on 27.04.17.
+ */
+public interface RelationService {
+
+    ListenableFuture<Boolean> checkRelation(EntityId from, EntityId to, String relationType);
+
+    ListenableFuture<Boolean> saveRelation(EntityRelation relation);
+
+    ListenableFuture<Boolean> deleteRelation(EntityRelation relation);
+
+    ListenableFuture<Boolean> deleteRelation(EntityId from, EntityId to, String relationType);
+
+    ListenableFuture<Boolean> deleteEntityRelations(EntityId entity);
+
+    ListenableFuture<List<EntityRelation>> findByFrom(EntityId from);
+
+    ListenableFuture<List<EntityRelation>> findByFromAndType(EntityId from, String relationType);
+
+    ListenableFuture<List<EntityRelation>> findByTo(EntityId to);
+
+    ListenableFuture<List<EntityRelation>> findByToAndType(EntityId to, String relationType);
+
+    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/relation/RelationsSearchParameters.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java
new file mode 100644
index 0000000..65920d7
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationsSearchParameters.java
@@ -0,0 +1,48 @@
+/**
+ * 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.relation;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+
+import java.util.UUID;
+
+/**
+ * Created by ashvayka on 03.05.17.
+ */
+@Data
+@AllArgsConstructor
+public class RelationsSearchParameters {
+
+    private UUID rootId;
+    private EntityType rootType;
+    private EntitySearchDirection direction;
+    private int maxLevel = 1;
+
+    public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel) {
+        this.rootId = entityId.getId();
+        this.rootType = entityId.getEntityType();
+        this.direction = direction;
+        this.maxLevel = maxLevel;
+    }
+
+    public EntityId getEntityId() {
+        return EntityIdFactory.getByTypeAndUuid(rootType, rootId);
+    }
+}
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 87e35ed..1bc0d92 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
@@ -17,10 +17,13 @@ package org.thingsboard.server.dao.rule;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
+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.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;
@@ -31,9 +34,11 @@ 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.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.plugin.PluginService;
 import org.thingsboard.server.dao.service.DataValidator;
 import org.thingsboard.server.dao.service.PaginatedRemover;
@@ -50,7 +55,7 @@ import static org.thingsboard.server.dao.service.Validator.validatePageLink;
 
 @Service
 @Slf4j
-public class BaseRuleService implements RuleService {
+public class BaseRuleService extends BaseEntityService implements RuleService {
 
     private final TenantId systemTenantId = new TenantId(NULL_UUID);
 
@@ -164,6 +169,12 @@ public class BaseRuleService implements RuleService {
     }
 
     @Override
+    public ListenableFuture<RuleMetaData> findRuleByIdAsync(RuleId ruleId) {
+        validateId(ruleId, "Incorrect rule id for search rule request.");
+        return ruleDao.findByIdAsync(ruleId.getId());
+    }
+
+    @Override
     public List<RuleMetaData> findPluginRules(String pluginToken) {
         return ruleDao.findRulesByPlugin(pluginToken);
     }
@@ -228,6 +239,7 @@ public class BaseRuleService implements RuleService {
     @Override
     public void deleteRuleById(RuleId ruleId) {
         validateId(ruleId, "Incorrect rule id for delete rule request.");
+        deleteEntityRelations(ruleId);
         ruleDao.deleteById(ruleId);
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java
index 98ff8c0..23f4a01 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.rule;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -29,6 +30,8 @@ public interface RuleService {
 
     RuleMetaData findRuleById(RuleId ruleId);
 
+    ListenableFuture<RuleMetaData> findRuleByIdAsync(RuleId ruleId);
+
     List<RuleMetaData> findPluginRules(String pluginToken);
 
     TextPageData<RuleMetaData> findSystemRules(TextPageLink pageLink);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
index 70e9860..bb4912e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.service;
 
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -25,6 +26,19 @@ import java.util.UUID;
 public class Validator {
 
     /**
+     * This method validate <code>EntityId</code> entity id. If entity id is invalid than throw
+     * <code>IncorrectParameterException</code> exception
+     *
+     * @param entityId          the entityId
+     * @param errorMessage the error message for exception
+     */
+    public static void validateEntityId(EntityId entityId, String errorMessage) {
+        if (entityId == null || entityId.getId() == null) {
+            throw new IncorrectParameterException(errorMessage);
+        }
+    }
+
+    /**
      * This method validate <code>String</code> string. If string is invalid than throw
      * <code>IncorrectParameterException</code> exception
      *
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
index 3dfaccd..9b6c4d1 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/JpaCustomerDao.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.dao.model.sql.CustomerEntity;
 import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
 
 import java.util.List;
+import java.util.Optional;
 import java.util.UUID;
 
 /**
@@ -59,4 +60,10 @@ public class JpaCustomerDao extends JpaAbstractSearchTextDao<CustomerEntity, Cus
                     pageLink.getTextSearch(), pageLink.getIdOffset()));
         }
     }
+
+    @Override
+    public Optional<Customer> findCustomersByTenantIdAndTitle(UUID tenantId, String title) {
+        // TODO: vsosliuk implement
+        return null;
+    }
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
index 7067a55..b0d9a71 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java
@@ -35,5 +35,4 @@ public interface EventRepository extends CrudRepository<EventEntity, UUID>, JpaS
 
     EventEntity findByTenantIdAndEntityTypeAndEntityIdAndEventTypeAndEventUid(
             UUID tenantId, EntityType entityType, UUID id, String eventType, String eventUid);
-
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
index 13b5090..21c8001 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.tenant;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
@@ -23,6 +24,8 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 public interface TenantService {
 
     Tenant findTenantById(TenantId tenantId);
+
+    ListenableFuture<Tenant> findTenantByIdAsync(TenantId customerId);
     
     Tenant saveTenant(Tenant tenant);
     
@@ -31,5 +34,4 @@ public interface TenantService {
     TextPageData<Tenant> findTenants(TextPageLink pageLink);
     
     void deleteTenants();
-    
 }
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 976cb71..e80af2f 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
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.dao.tenant;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +27,7 @@ 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.exception.DataValidationException;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.rule.RuleService;
@@ -37,21 +39,23 @@ import org.thingsboard.server.dao.widget.WidgetsBundleService;
 
 import java.util.List;
 
+import static org.thingsboard.server.dao.service.Validator.validateId;
+
 @Service
 @Slf4j
-public class TenantServiceImpl implements TenantService {
-    
+public class TenantServiceImpl extends BaseEntityService implements TenantService {
+
     private static final String DEFAULT_TENANT_REGION = "Global";
 
     @Autowired
     private TenantDao tenantDao;
-    
+
     @Autowired
     private UserService userService;
-    
+
     @Autowired
     private CustomerService customerService;
-    
+
     @Autowired
     private DeviceService deviceService;
 
@@ -66,7 +70,7 @@ public class TenantServiceImpl implements TenantService {
 
     @Autowired
     private PluginService pluginService;
-    
+
     @Override
     public Tenant findTenantById(TenantId tenantId) {
         log.trace("Executing findTenantById [{}]", tenantId);
@@ -75,6 +79,13 @@ public class TenantServiceImpl implements TenantService {
     }
 
     @Override
+    public ListenableFuture<Tenant> findTenantByIdAsync(TenantId tenantId) {
+        log.trace("Executing TenantIdAsync [{}]", tenantId);
+        validateId(tenantId, "Incorrect tenantId " + tenantId);
+        return tenantDao.findByIdAsync(tenantId.getId());
+    }
+
+    @Override
     public Tenant saveTenant(Tenant tenant) {
         log.trace("Executing saveTenant [{}]", tenant);
         tenant.setRegion(DEFAULT_TENANT_REGION);
@@ -94,6 +105,7 @@ public class TenantServiceImpl implements TenantService {
         ruleService.deleteRulesByTenantId(tenantId);
         pluginService.deletePluginsByTenantId(tenantId);
         tenantDao.removeById(tenantId.getId());
+        deleteEntityRelations(tenantId);
     }
 
     @Override
@@ -122,10 +134,10 @@ public class TenantServiceImpl implements TenantService {
                     }
                 }
     };
-    
+
     private PaginatedRemover<String, Tenant> tenantsRemover =
             new PaginatedRemover<String, Tenant>() {
-        
+
         @Override
         protected List<Tenant> findEntities(String region, TextPageLink pageLink) {
             return tenantDao.findTenantsByRegion(region, pageLink);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
index 7a59dcc..033fa5f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
@@ -24,7 +24,7 @@ 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.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.common.data.kv.TsKvQuery;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -49,30 +49,30 @@ public class BaseTimeseriesService implements TimeseriesService {
     private TimeseriesDao timeseriesDao;
 
     @Override
-    public ListenableFuture<List<TsKvEntry>> findAll(String entityType, UUIDBased entityId, List<TsKvQuery> queries) {
-        validate(entityType, entityId);
+    public ListenableFuture<List<TsKvEntry>> findAll(EntityId entityId, List<TsKvQuery> queries) {
+        validate(entityId);
         queries.forEach(query -> validate(query));
-        return timeseriesDao.findAllAsync(entityType, entityId.getId(), queries);
+        return timeseriesDao.findAllAsync(entityId, queries);
     }
 
     @Override
-    public ListenableFuture<List<ResultSet>> findLatest(String entityType, UUIDBased entityId, Collection<String> keys) {
-        validate(entityType, entityId);
+    public ListenableFuture<List<ResultSet>> findLatest(EntityId entityId, Collection<String> keys) {
+        validate(entityId);
         List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(keys.size());
         keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key));
-        keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityType, entityId.getId(), key)));
+        keys.forEach(key -> futures.add(timeseriesDao.findLatest(entityId, key)));
         return Futures.allAsList(futures);
     }
 
     @Override
-    public ResultSetFuture findAllLatest(String entityType, UUIDBased entityId) {
-        validate(entityType, entityId);
-        return timeseriesDao.findAllLatest(entityType, entityId.getId());
+    public ResultSetFuture findAllLatest(EntityId entityId) {
+        validate(entityId);
+        return timeseriesDao.findAllLatest(entityId);
     }
 
     @Override
-    public ListenableFuture<List<ResultSet>> save(String entityType, UUIDBased entityId, TsKvEntry tsKvEntry) {
-        validate(entityType, entityId);
+    public ListenableFuture<List<ResultSet>> save(EntityId entityId, TsKvEntry tsKvEntry) {
+        validate(entityId);
         if (tsKvEntry == null) {
             throw new IncorrectParameterException("Key value entry can't be null");
         }
@@ -80,13 +80,13 @@ public class BaseTimeseriesService implements TimeseriesService {
         long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
 
         List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
-        saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs);
+        saveAndRegisterFutures(futures, entityId, tsKvEntry, partitionTs);
         return Futures.allAsList(futures);
     }
 
     @Override
-    public ListenableFuture<List<ResultSet>> save(String entityType, UUIDBased entityId, List<TsKvEntry> tsKvEntries) {
-        validate(entityType, entityId);
+    public ListenableFuture<List<ResultSet>> save(EntityId entityId, List<TsKvEntry> tsKvEntries) {
+        validate(entityId);
         List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(tsKvEntries.size() * INSERTS_PER_ENTRY);
         for (TsKvEntry tsKvEntry : tsKvEntries) {
             if (tsKvEntry == null) {
@@ -94,7 +94,7 @@ public class BaseTimeseriesService implements TimeseriesService {
             }
             UUID uid = entityId.getId();
             long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
-            saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs);
+            saveAndRegisterFutures(futures, entityId, tsKvEntry, partitionTs);
         }
         return Futures.allAsList(futures);
     }
@@ -109,15 +109,14 @@ public class BaseTimeseriesService implements TimeseriesService {
         return timeseriesDao.convertResultToTsKvEntryList(rs.all());
     }
 
-    private void saveAndRegisterFutures(List<ResultSetFuture> futures, String entityType, TsKvEntry tsKvEntry, UUID uid, long partitionTs) {
-        futures.add(timeseriesDao.savePartition(entityType, uid, partitionTs, tsKvEntry.getKey()));
-        futures.add(timeseriesDao.saveLatest(entityType, uid, tsKvEntry));
-        futures.add(timeseriesDao.save(entityType, uid, partitionTs, tsKvEntry));
+    private void saveAndRegisterFutures(List<ResultSetFuture> futures, EntityId entityId, TsKvEntry tsKvEntry, long partitionTs) {
+        futures.add(timeseriesDao.savePartition(entityId, partitionTs, tsKvEntry.getKey()));
+        futures.add(timeseriesDao.saveLatest(entityId, tsKvEntry));
+        futures.add(timeseriesDao.save(entityId, partitionTs, tsKvEntry));
     }
 
-    private static void validate(String entityType, UUIDBased entityId) {
-        Validator.validateString(entityType, "Incorrect entityType " + entityType);
-        Validator.validateId(entityId, "Incorrect entityId " + entityId);
+    private static void validate(EntityId entityId) {
+        Validator.validateEntityId(entityId, "Incorrect entityId " + entityId);
     }
 
     private static void validate(TsKvQuery query) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
index 738c5aa..d6c7443 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
@@ -26,6 +26,7 @@ import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.kv.*;
 import org.thingsboard.server.common.data.kv.DataType;
 import org.thingsboard.server.dao.CassandraAbstractAsyncDao;
@@ -93,8 +94,8 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
     }
 
     @Override
-    public ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, List<TsKvQuery> queries) {
-        List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(entityType, entityId, query)).collect(Collectors.toList());
+    public ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, List<TsKvQuery> queries) {
+        List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(entityId, query)).collect(Collectors.toList());
         return Futures.transform(Futures.allAsList(futures), new Function<List<List<TsKvEntry>>, List<TsKvEntry>>() {
             @Nullable
             @Override
@@ -107,9 +108,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
     }
 
 
-    private ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, TsKvQuery query) {
+    private ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, TsKvQuery query) {
         if (query.getAggregation() == Aggregation.NONE) {
-            return findAllAsyncWithLimit(entityType, entityId, query);
+            return findAllAsyncWithLimit(entityId, query);
         } else {
             long step = Math.max(query.getInterval(), minAggregationStepMs);
             long stepTs = query.getStartTs();
@@ -118,7 +119,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
                 long startTs = stepTs;
                 long endTs = stepTs + step;
                 TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation());
-                futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
+                futures.add(findAndAggregateAsync(entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
                 stepTs = endTs;
             }
             ListenableFuture<List<Optional<TsKvEntry>>> future = Futures.allAsList(futures);
@@ -132,11 +133,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
         }
     }
 
-    private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(String entityType, UUID entityId, TsKvQuery query) {
+    private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(EntityId entityId, TsKvQuery query) {
         long minPartition = toPartitionTs(query.getStartTs());
         long maxPartition = toPartitionTs(query.getEndTs());
 
-        ResultSetFuture partitionsFuture = fetchPartitions(entityType, entityId, query.getKey(), minPartition, maxPartition);
+        ResultSetFuture partitionsFuture = fetchPartitions(entityId, query.getKey(), minPartition, maxPartition);
 
         final SimpleListenableFuture<List<TsKvEntry>> resultFuture = new SimpleListenableFuture<>();
         final ListenableFuture<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
@@ -144,13 +145,13 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
         Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
             @Override
             public void onSuccess(@Nullable List<Long> partitions) {
-                TsKvQueryCursor cursor = new TsKvQueryCursor(entityType, entityId, query, partitions);
+                TsKvQueryCursor cursor = new TsKvQueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions);
                 findAllAsyncSequentiallyWithLimit(cursor, resultFuture);
             }
 
             @Override
             public void onFailure(Throwable t) {
-                log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityType, entityId, minPartition, maxPartition, t);
+                log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t);
             }
         }, readResultsProcessingExecutor);
 
@@ -186,19 +187,19 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
         }
     }
 
-    private ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(String entityType, UUID entityId, TsKvQuery query, long minPartition, long maxPartition) {
+    private ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(EntityId entityId, TsKvQuery query, long minPartition, long maxPartition) {
         final Aggregation aggregation = query.getAggregation();
         final String key = query.getKey();
         final long startTs = query.getStartTs();
         final long endTs = query.getEndTs();
         final long ts = startTs + (endTs - startTs) / 2;
 
-        ResultSetFuture partitionsFuture = fetchPartitions(entityType, entityId, key, minPartition, maxPartition);
+        ResultSetFuture partitionsFuture = fetchPartitions(entityId, key, minPartition, maxPartition);
 
         ListenableFuture<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
 
         ListenableFuture<List<ResultSet>> aggregationChunks = Futures.transform(partitionsListFuture,
-                getFetchChunksAsyncFunction(entityType, entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor);
+                getFetchChunksAsyncFunction(entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor);
 
         return Futures.transform(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts), readResultsProcessingExecutor);
     }
@@ -208,21 +209,21 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
                 .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).collect(Collectors.toList());
     }
 
-    private AsyncFunction<List<Long>, List<ResultSet>> getFetchChunksAsyncFunction(String entityType, UUID entityId, String key, Aggregation aggregation, long startTs, long endTs) {
+    private AsyncFunction<List<Long>, List<ResultSet>> getFetchChunksAsyncFunction(EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) {
         return partitions -> {
             try {
                 PreparedStatement proto = getFetchStmt(aggregation);
                 List<ResultSetFuture> futures = new ArrayList<>(partitions.size());
                 for (Long partition : partitions) {
-                    log.trace("Fetching data for partition [{}] for entityType {} and entityId {}", partition, entityType, entityId);
+                    log.trace("Fetching data for partition [{}] for entityType {} and entityId {}", partition, entityId.getEntityType(), entityId.getId());
                     BoundStatement stmt = proto.bind();
-                    stmt.setString(0, entityType);
-                    stmt.setUUID(1, entityId);
+                    stmt.setString(0, entityId.getEntityType().name());
+                    stmt.setUUID(1, entityId.getId());
                     stmt.setString(2, key);
                     stmt.setLong(3, partition);
                     stmt.setLong(4, startTs);
                     stmt.setLong(5, endTs);
-                    log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId);
+                    log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId());
                     futures.add(executeAsyncRead(stmt));
                 }
                 return Futures.allAsList(futures);
@@ -234,30 +235,30 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
     }
 
     @Override
-    public ResultSetFuture findLatest(String entityType, UUID entityId, String key) {
+    public ResultSetFuture findLatest(EntityId entityId, String key) {
         BoundStatement stmt = getFindLatestStmt().bind();
-        stmt.setString(0, entityType);
-        stmt.setUUID(1, entityId);
+        stmt.setString(0, entityId.getEntityType().name());
+        stmt.setUUID(1, entityId.getId());
         stmt.setString(2, key);
-        log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId);
+        log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId());
         return executeAsyncRead(stmt);
     }
 
     @Override
-    public ResultSetFuture findAllLatest(String entityType, UUID entityId) {
+    public ResultSetFuture findAllLatest(EntityId entityId) {
         BoundStatement stmt = getFindAllLatestStmt().bind();
-        stmt.setString(0, entityType);
-        stmt.setUUID(1, entityId);
-        log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId);
+        stmt.setString(0, entityId.getEntityType().name());
+        stmt.setUUID(1, entityId.getId());
+        log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityId.getEntityType(), entityId.getId());
         return executeAsyncRead(stmt);
     }
 
     @Override
-    public ResultSetFuture save(String entityType, UUID entityId, long partition, TsKvEntry tsKvEntry) {
+    public ResultSetFuture save(EntityId entityId, long partition, TsKvEntry tsKvEntry) {
         DataType type = tsKvEntry.getDataType();
         BoundStatement stmt = getSaveStmt(type).bind()
-                .setString(0, entityType)
-                .setUUID(1, entityId)
+                .setString(0, entityId.getEntityType().name())
+                .setUUID(1, entityId.getId())
                 .setString(2, tsKvEntry.getKey())
                 .setLong(3, partition)
                 .setLong(4, tsKvEntry.getTs());
@@ -266,11 +267,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
     }
 
     @Override
-    public ResultSetFuture saveLatest(String entityType, UUID entityId, TsKvEntry tsKvEntry) {
+    public ResultSetFuture saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
         DataType type = tsKvEntry.getDataType();
         BoundStatement stmt = getLatestStmt(type).bind()
-                .setString(0, entityType)
-                .setUUID(1, entityId)
+                .setString(0, entityId.getEntityType().name())
+                .setUUID(1, entityId.getId())
                 .setString(2, tsKvEntry.getKey())
                 .setLong(3, tsKvEntry.getTs());
         addValue(tsKvEntry, stmt, 4);
@@ -278,11 +279,11 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
     }
 
     @Override
-    public ResultSetFuture savePartition(String entityType, UUID entityId, long partition, String key) {
-        log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityType, entityId, key);
+    public ResultSetFuture savePartition(EntityId entityId, long partition, String key) {
+        log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key);
         return executeAsyncWrite(getPartitionInsertStmt().bind()
-                .setString(0, entityType)
-                .setUUID(1, entityId)
+                .setString(0, entityId.getEntityType().name())
+                .setUUID(1, entityId.getId())
                 .setLong(2, partition)
                 .setString(3, key));
     }
@@ -338,9 +339,9 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
      * Select existing partitions from the table
      * <code>{@link ModelConstants#TS_KV_PARTITIONS_CF}</code> for the given entity
      */
-    private ResultSetFuture fetchPartitions(String entityType, UUID entityId, String key, long minPartition, long maxPartition) {
-        Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityType))
-                .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId)).and(eq(ModelConstants.KEY_COLUMN, key));
+    private ResultSetFuture fetchPartitions(EntityId entityId, String key, long minPartition, long maxPartition) {
+        Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityId.getEntityType().name()))
+                .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId.getId())).and(eq(ModelConstants.KEY_COLUMN, key));
         select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
         select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
         return executeAsyncRead(select);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
index 452eb2c..b564cca 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.timeseries;
 import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.Row;
 import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.common.data.kv.TsKvQuery;
 
@@ -31,17 +32,17 @@ public interface TimeseriesDao {
 
     long toPartitionTs(long ts);
 
-    ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, List<TsKvQuery> queries);
+    ListenableFuture<List<TsKvEntry>> findAllAsync(EntityId entityId, List<TsKvQuery> queries);
 
-    ResultSetFuture findLatest(String entityType, UUID entityId, String key);
+    ResultSetFuture findLatest(EntityId entityId, String key);
 
-    ResultSetFuture findAllLatest(String entityType, UUID entityId);
+    ResultSetFuture findAllLatest(EntityId entityId);
 
-    ResultSetFuture save(String entityType, UUID entityId, long partition, TsKvEntry tsKvEntry);
+    ResultSetFuture save(EntityId entityId, long partition, TsKvEntry tsKvEntry);
 
-    ResultSetFuture savePartition(String entityType, UUID entityId, long partition, String key);
+    ResultSetFuture savePartition(EntityId entityId, long partition, String key);
 
-    ResultSetFuture saveLatest(String entityType, UUID entityId, TsKvEntry tsKvEntry);
+    ResultSetFuture saveLatest(EntityId entityId, TsKvEntry tsKvEntry);
 
     TsKvEntry convertResultToTsKvEntry(Row row);
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
index 20731b2..9c4a224 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
@@ -19,6 +19,7 @@ import com.datastax.driver.core.ResultSet;
 import com.datastax.driver.core.ResultSetFuture;
 import com.datastax.driver.core.Row;
 import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 import org.thingsboard.server.common.data.kv.TsKvEntry;
 import org.thingsboard.server.common.data.kv.TsKvQuery;
@@ -31,15 +32,15 @@ import java.util.List;
  */
 public interface TimeseriesService {
 
-    ListenableFuture<List<TsKvEntry>> findAll(String entityType, UUIDBased entityId, List<TsKvQuery> queries);
+    ListenableFuture<List<TsKvEntry>> findAll(EntityId entityId, List<TsKvQuery> queries);
 
-    ListenableFuture<List<ResultSet>> findLatest(String entityType, UUIDBased entityId, Collection<String> keys);
+    ListenableFuture<List<ResultSet>> findLatest(EntityId entityId, Collection<String> keys);
 
-    ResultSetFuture findAllLatest(String entityType, UUIDBased entityId);
+    ResultSetFuture findAllLatest(EntityId entityId);
 
-    ListenableFuture<List<ResultSet>> save(String entityType, UUIDBased entityId, TsKvEntry tsKvEntry);
+    ListenableFuture<List<ResultSet>> save(EntityId entityId, TsKvEntry tsKvEntry);
 
-    ListenableFuture<List<ResultSet>> save(String entityType, UUIDBased entityId, List<TsKvEntry> tsKvEntry);
+    ListenableFuture<List<ResultSet>> save(EntityId entityId, List<TsKvEntry> tsKvEntry);
 
     TsKvEntry convertResultToTsKvEntry(Row row);
 
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 7f4d6c0..709e62d 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
@@ -31,6 +31,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.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
@@ -44,7 +45,7 @@ import static org.thingsboard.server.dao.service.Validator.*;
 
 @Service
 @Slf4j
-public class UserServiceImpl implements UserService {
+public class UserServiceImpl extends BaseEntityService implements UserService {
 
     private static final int DEFAULT_TOKEN_LENGTH = 30;
 
@@ -159,6 +160,7 @@ public class UserServiceImpl implements UserService {
         validateId(userId, "Incorrect userId " + userId);
         UserCredentials userCredentials = userCredentialsDao.findByUserId(userId.getId());
         userCredentialsDao.removeById(userCredentials.getUuidId());
+        deleteEntityRelations(userId);
         userDao.removeById(userId.getId());
     }
 
diff --git a/dao/src/main/resources/schema.cql b/dao/src/main/resources/schema.cql
index febd1e9..b86031b 100644
--- a/dao/src/main/resources/schema.cql
+++ b/dao/src/main/resources/schema.cql
@@ -137,6 +137,13 @@ CREATE TABLE IF NOT EXISTS thingsboard.customer (
 	PRIMARY KEY (id, tenant_id)
 );
 
+CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_title AS
+	SELECT *
+	from thingsboard.customer
+	WHERE tenant_id IS NOT NULL AND title IS NOT NULL AND id IS NOT NULL
+	PRIMARY KEY ( tenant_id, title, id )
+	WITH CLUSTERING ORDER BY ( title ASC, id DESC );
+
 CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.customer_by_tenant_and_search_text AS
 	SELECT *
 	from thingsboard.customer
@@ -195,6 +202,81 @@ 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)
+);
+
+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);
+
+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);
+
+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 );
+
+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 text,
+	additional_info text,
+	PRIMARY KEY ((from_id, from_type), relation_type, to_id, to_type)
+);
+
+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);
+
 CREATE TABLE IF NOT EXISTS thingsboard.widgets_bundle (
     id timeuuid,
     tenant_id timeuuid,
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index cd00b1d..fc7c315 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -80,7 +80,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'gpio_widgets', 'gpio_panel',
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
 VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'timeseries_table',
-'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<md-tabs md-selected=\"sourceIndex\" ng-class=\"{''tb-headless'': sources.length === 1}\"\n    id=\"tabs\" md-border-bottom flex class=\"tb-absolute-fill\">\n    <md-tab ng-repeat=\"source in sources\" label=\"{{ source.datasource.name }}\">\n        <md-table-container>\n            <table md-table>\n                <thead md-head md-order=\"source.query.order\" md-on-reorder=\"onReorder(source)\">\n                    <tr md-row>\n                        <th ng-show=\"showTimestamp\" md-column md-order-by=\"0\"><span>Timestamp</span></th>\n                        <th md-column md-order-by=\"{{ h.index }}\" ng-repeat=\"h in source.ts.header\"><span>{{ h.dataKey.label }}</span></th>\n                    </tr>\n                </thead>\n                <tbody md-body>\n                    <tr md-row ng-repeat=\"row in source.ts.data\">\n                        <td ng-show=\"$index > 0 || ($index === 0 && showTimestamp)\" md-cell ng-repeat=\"d in row track by $index\" ng-style=\"cellStyle(source, $index, d)\" ng-bind-html=\"cellContent(source, $index, row, d)\">\n                        </td>\n                    </tr>    \n                </tbody>    \n            </table>\n        </md-table-container>\n        <md-table-pagination md-limit=\"source.query.limit\" md-limit-options=\"[5, 10, 15]\"\n                             md-page=\"source.query.page\" md-total=\"{{source.ts.count}}\"\n                             md-on-paginate=\"onPaginate(source)\" md-page-select>\n        </md-table-pagination>\n    </md-tab>\n</md-tabs>","templateCss":"table.md-table thead.md-head>tr.md-row {\n    height: 40px;\n}\n\ntable.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {\n    height: 38px;\n}\n\n.md-table-pagination>* {\n    height: 46px;\n}\n","controllerScript":"self.onInit = function() {\n    \n    var scope = self.ctx.$scope;\n    \n    self.ctx.filter = scope.$injector.get(\"$filter\");\n\n    scope.sources = [];\n    scope.sourceIndex = 0;\n    scope.showTimestamp = self.ctx.settings.showTimestamp !== false;\n    var origColor = self.ctx.widgetConfig.color || ''rgba(0, 0, 0, 0.87)'';\n    var defaultColor = tinycolor(origColor);\n    var mdDark = defaultColor.setAlpha(0.87).toRgbString();\n    var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();\n    var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();\n    var mdDarkIcon = mdDarkSecondary;\n    var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();\n    \n    var cssString = ''table.md-table th.md-column {\\n''+\n    ''color: '' + mdDarkSecondary + '';\\n''+\n    ''}\\n''+\n    ''table.md-table th.md-column md-icon.md-sort-icon {\\n''+\n    ''color: '' + mdDarkDisabled + '';\\n''+\n    ''}\\n''+\n    ''table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\\n''+\n    ''color: '' + mdDark + '';\\n''+\n    ''}\\n''+\n    ''table.md-table td.md-cell {\\n''+\n    ''color: '' + mdDark + '';\\n''+\n    ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n    ''}\\n''+\n    ''table.md-table td.md-cell.md-placeholder {\\n''+\n    ''color: '' + mdDarkDisabled + '';\\n''+\n    ''}\\n''+\n    ''table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\\n''+\n    ''color: '' + mdDarkSecondary + '';\\n''+\n    ''}\\n''+\n    ''.md-table-pagination {\\n''+\n    ''color: '' + mdDarkSecondary + '';\\n''+\n    ''border-top: 1px ''+mdDarkDivider+'' solid;\\n''+\n    ''}\\n''+\n    ''.md-table-pagination .buttons md-icon {\\n''+\n    ''color: '' + mdDarkSecondary + '';\\n''+\n    ''}\\n''+\n    ''.md-table-pagination md-select:not([disabled]):focus .md-select-value {\\n''+\n    ''color: '' + mdDarkSecondary + '';\\n''+\n    ''}'';\n    \n    var cssParser = new cssjs();\n    cssParser.testMode = false;\n    var namespace = ''ts-table-'' + hashCode(cssString);\n    cssParser.cssPreviewNamespace = namespace;\n    cssParser.createStyleElement(namespace, cssString);\n    self.ctx.$container.addClass(namespace);\n    \n    function hashCode(str) {\n        var hash = 0;\n        var i, char;\n        if (str.length === 0) return hash;\n        for (i = 0; i < str.length; i++) {\n            char = str.charCodeAt(i);\n            hash = ((hash << 5) - hash) + char;\n            hash = hash & hash;\n        }\n        return hash;\n    }\n    \n    var keyOffset = 0;\n    for (var ds = 0; ds < self.ctx.datasources.length; ds++) {\n        var source = {};\n        var datasource = self.ctx.datasources[ds];\n        source.keyStartIndex = keyOffset;\n        keyOffset += datasource.dataKeys.length;\n        source.keyEndIndex = keyOffset;\n        source.datasource = datasource;\n        source.data = [];\n        source.rawData = [];\n        source.query = {\n            limit: 5,\n            page: 1,\n            order: ''-0''\n        }\n        source.ts = {\n            header: [],\n            count: 0,\n            data: [],\n            stylesInfo: [],\n            contentsInfo: [],\n            rowDataTemplate: {}\n        }\n        source.ts.rowDataTemplate[''Timestamp''] = null;\n        for (var a = 0; a < datasource.dataKeys.length; a++ ) {\n            var dataKey = datasource.dataKeys[a];\n            var keySettings = dataKey.settings;\n            source.ts.header.push({\n                index: a+1,\n                dataKey: dataKey\n            });\n            source.ts.rowDataTemplate[dataKey.label] = null;\n\n            var cellStyleFunction = null;\n            var useCellStyleFunction = false;\n            \n            if (keySettings.useCellStyleFunction === true) {\n                if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {\n                    try {\n                       cellStyleFunction = new Function(''value'', keySettings.cellStyleFunction);\n                       useCellStyleFunction = true;\n                    } catch (e) {\n                       cellStyleFunction = null;\n                       useCellStyleFunction = false;\n                    }\n                }\n            }\n\n            source.ts.stylesInfo.push({\n                useCellStyleFunction: useCellStyleFunction,\n                cellStyleFunction: cellStyleFunction\n            });\n            \n            var cellContentFunction = null;\n            var useCellContentFunction = false;\n            \n            if (keySettings.useCellContentFunction === true) {\n                if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {\n                    try {\n                       cellContentFunction = new Function(''value, rowData, filter'', keySettings.cellContentFunction);\n                       useCellContentFunction = true;\n                    } catch (e) {\n                       cellContentFunction = null;\n                       useCellContentFunction = false;\n                    }\n                }\n            }\n            \n            source.ts.contentsInfo.push({\n                useCellContentFunction: useCellContentFunction,\n                cellContentFunction: cellContentFunction\n            });\n            \n        }\n        scope.sources.push(source);\n    }\n\n    scope.onPaginate = function(source) {\n        updatePage(source);\n    }\n    \n    scope.onReorder = function(source) {\n        reorder(source);\n        updatePage(source);\n    }\n    \n    scope.cellStyle = function(source, index, value) {\n        var style = {};\n        if (index > 0) {\n            var styleInfo = source.ts.stylesInfo[index-1];\n            if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {\n                try {\n                    style = styleInfo.cellStyleFunction(value);\n                } catch (e) {\n                    style = {};\n                }\n            }\n        }\n        return style;\n    }\n\n    scope.cellContent = function(source, index, row, value) {\n        if (index === 0) {\n            return self.ctx.filter(''date'')(value, ''yyyy-MM-dd HH:mm:ss'');\n        } else {\n            var strContent = '''';\n            if (angular.isDefined(value)) {\n                strContent = ''''+value;\n            }\n            var content = strContent;\n            var contentInfo = source.ts.contentsInfo[index-1];\n            if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {\n                try {\n                    var rowData = source.ts.rowDataTemplate;\n                    rowData[''Timestamp''] = row[0];\n                    for (var h=0; h < source.ts.header.length; h++) {\n                        var headerInfo = source.ts.header[h];\n                        rowData[headerInfo.dataKey.name] = row[headerInfo.index];\n                    }\n                    content = contentInfo.cellContentFunction(value, rowData, self.ctx.filter);\n                } catch (e) {\n                    content = strContent;\n                }\n            }            \n            return content;\n        }\n    }\n    \n    scope.$watch(''sourceIndex'', function(newIndex, oldIndex) {\n       if (newIndex != oldIndex) {\n           updateSourceData(scope.sources[scope.sourceIndex]);\n       } \n    });\n}\n\nself.onDataUpdated = function() {\n    var scope = self.ctx.$scope;\n    for (var s=0; s < scope.sources.length; s++) {\n        var source = scope.sources[s];\n        source.rawData = self.ctx.data.slice(source.keyStartIndex, source.keyEndIndex);\n    }\n    updateSourceData(scope.sources[scope.sourceIndex]);\n    scope.$digest();\n}\n\nself.onDestroy = function() {\n}\n\nfunction updatePage(source) {\n    var startIndex = source.query.limit * (source.query.page - 1);\n    source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);\n}\n\nfunction reorder(source) {\n    source.data = self.ctx.filter(''orderBy'')(source.data, source.query.order);\n}\n\nfunction convertData(data) {\n    var rowsMap = {};\n    for (var d = 0; d < data.length; d++) {\n        var columnData = data[d].data;\n        for (var i = 0; i < columnData.length; i++) {\n            var cellData = columnData[i];\n            var timestamp = cellData[0];\n            var row = rowsMap[timestamp];\n            if (!row) {\n                row = [];\n                row[0] = timestamp;\n                for (var c = 0; c < data.length; c++) {\n                    row[c+1] = undefined;\n                }\n                rowsMap[timestamp] = row;\n            }\n            row[d+1] = cellData[1];\n        }\n    }\n    var rows = [];\n    for (var t in rowsMap) {\n        rows.push(rowsMap[t]);\n    }\n    return rows;\n}\n\nfunction updateSourceData(source) {\n    source.data = convertData(source.rawData);\n    source.ts.count = source.data.length;\n    reorder(source);\n    updatePage(source);\n}\n","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"TimeseriesTableSettings\",\n        \"properties\": {\n            \"showTimestamp\": {\n                \"title\": \"Display timestamp column\",\n                \"type\": \"boolean\",\n                \"default\": true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"showTimestamp\"\n    ]\n}","dataKeySettingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"useCellContentFunction\": {\n                \"title\": \"Use cell content function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellContentFunction\": {\n                \"title\": \"Cell content function: f(value, rowData, filter)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        },\n        \"useCellContentFunction\",\n        {\n            \"key\": \"cellContentFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature  °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = (value + 60)/120 * 100;\\n    var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n    color.setAlpha(.5);\\n    return {\\n      paddingLeft: ''20px'',\\n      color: ''#ffffff'',\\n      background: color.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = value;\\n    var backgroundColor = tinycolor(''blue'');\\n    backgroundColor.setAlpha(value/100);\\n    var color = ''blue'';\\n    if (value > 50) {\\n        color = ''white'';\\n    }\\n    \\n    return {\\n      paddingLeft: ''20px'',\\n      color: color,\\n      background: backgroundColor.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}',
+'{"type":"timeseries","sizeX":8,"sizeY":6.5,"resources":[],"templateHtml":"<tb-timeseries-table-widget \n    config=\"config\"\n    table-id=\"tableId\"\n    datasources=\"datasources\"\n    data=\"data\">\n</tb-timeseries-table-widget>","templateCss":"","controllerScript":"self.onInit = function() {\n    \n    var scope = self.ctx.$scope;\n    var id = self.ctx.$scope.$injector.get(''utils'').guid();\n\n    scope.config = {\n        settings: self.ctx.settings,\n        widgetConfig: self.ctx.widgetConfig\n    }\n\n    scope.datasources = self.ctx.datasources;\n    scope.data = self.ctx.data;\n    scope.tableId = \"table-\"+id;\n    \n}\n\nself.onDataUpdated = function() {\n    self.ctx.$scope.data = self.ctx.data;\n    self.ctx.$scope.$broadcast(''timeseries-table-data-updated'', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"TimeseriesTableSettings\",\n        \"properties\": {\n            \"showTimestamp\": {\n                \"title\": \"Display timestamp column\",\n                \"type\": \"boolean\",\n                \"default\": true\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"showTimestamp\"\n    ]\n}","dataKeySettingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"useCellStyleFunction\": {\n                \"title\": \"Use cell style function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellStyleFunction\": {\n                \"title\": \"Cell style function: f(value)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"useCellContentFunction\": {\n                \"title\": \"Use cell content function\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"cellContentFunction\": {\n                \"title\": \"Cell content function: f(value, rowData, filter)\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"useCellStyleFunction\",\n        {\n            \"key\": \"cellStyleFunction\",\n            \"type\": \"javascript\"\n        },\n        \"useCellContentFunction\",\n        {\n            \"key\": \"cellContentFunction\",\n            \"type\": \"javascript\"\n        }\n    ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature  °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = (value + 60)/120 * 100;\\n    var color = tinycolor.mix(''blue'', ''red'', amount = percent);\\n    color.setAlpha(.5);\\n    return {\\n      paddingLeft: ''20px'',\\n      color: ''#ffffff'',\\n      background: color.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n    var percent = value;\\n    var backgroundColor = tinycolor(''blue'');\\n    backgroundColor.setAlpha(value/100);\\n    var color = ''blue'';\\n    if (value > 50) {\\n        color = ''white'';\\n    }\\n    \\n    return {\\n      paddingLeft: ''20px'',\\n      color: color,\\n      background: backgroundColor.toRgbString(),\\n      fontSize: ''18px''\\n    };\\n} else {\\n    return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}"}',
 'Timeseries table' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
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 f66e6ee..d724d7f 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
@@ -32,6 +32,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.test.context.support.AnnotationConfigContextLoader;
 import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UUIDBased;
@@ -40,6 +41,7 @@ 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.component.ComponentDescriptorService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
@@ -47,6 +49,7 @@ import org.thingsboard.server.dao.device.DeviceCredentialsService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.event.EventService;
 import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
 import org.thingsboard.server.dao.tenant.TenantService;
@@ -111,6 +114,12 @@ public abstract class AbstractServiceTest {
     protected EventService eventService;
 
     @Autowired
+    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..8b90521
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
@@ -0,0 +1,98 @@
+/**
+ * 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.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.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.findAlarmById(created.getId()).get();
+        Assert.assertEquals(created, fetched);
+    }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java
index 0271cad..96fdc47 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceImplTest.java
@@ -33,11 +33,11 @@ import java.util.Collections;
 import java.util.List;
 
 public class CustomerServiceImplTest extends AbstractServiceTest {
-    
+
     private IdComparator<Customer> idComparator = new IdComparator<>();
-    
+
     private TenantId tenantId;
-    
+
     @Before
     public void before() {
         Tenant tenant = new Tenant();
@@ -58,23 +58,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
         customer.setTenantId(tenantId);
         customer.setTitle("My customer");
         Customer savedCustomer = customerService.saveCustomer(customer);
-        
+
         Assert.assertNotNull(savedCustomer);
         Assert.assertNotNull(savedCustomer.getId());
         Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
         Assert.assertEquals(customer.getTenantId(), savedCustomer.getTenantId());
         Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
-        
-        
+
+
         savedCustomer.setTitle("My new customer");
-        
+
         customerService.saveCustomer(savedCustomer);
         Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId());
         Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle());
-        
+
         customerService.deleteCustomer(savedCustomer.getId());
     }
-    
+
     @Test
     public void testFindCustomerById() {
         Customer customer = new Customer();
@@ -86,21 +86,21 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
         Assert.assertEquals(savedCustomer, foundCustomer);
         customerService.deleteCustomer(savedCustomer.getId());
     }
-    
+
     @Test(expected = DataValidationException.class)
     public void testSaveCustomerWithEmptyTitle() {
         Customer customer = new Customer();
         customer.setTenantId(tenantId);
         customerService.saveCustomer(customer);
     }
-    
+
     @Test(expected = DataValidationException.class)
     public void testSaveCustomerWithEmptyTenant() {
         Customer customer = new Customer();
         customer.setTitle("My customer");
         customerService.saveCustomer(customer);
     }
-    
+
     @Test(expected = DataValidationException.class)
     public void testSaveCustomerWithInvalidTenant() {
         Customer customer = new Customer();
@@ -108,7 +108,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
         customer.setTenantId(new TenantId(UUIDs.timeBased()));
         customerService.saveCustomer(customer);
     }
-    
+
     @Test(expected = DataValidationException.class)
     public void testSaveCustomerWithInvalidEmail() {
         Customer customer = new Customer();
@@ -117,7 +117,7 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
         customer.setEmail("invalid@mail");
         customerService.saveCustomer(customer);
     }
-    
+
     @Test
     public void testDeleteCustomer() {
         Customer customer = new Customer();
@@ -128,23 +128,23 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
         Customer foundCustomer = customerService.findCustomerById(savedCustomer.getId());
         Assert.assertNull(foundCustomer);
     }
-    
+
     @Test
     public void testFindCustomersByTenantId() {
         Tenant tenant = new Tenant();
         tenant.setTitle("Test tenant");
         tenant = tenantService.saveTenant(tenant);
-        
+
         TenantId tenantId = tenant.getId();
-        
+
         List<Customer> customers = new ArrayList<>();
-        for (int i=0;i<135;i++) {
+        for (int i = 0; i < 135; i++) {
             Customer customer = new Customer();
             customer.setTenantId(tenantId);
-            customer.setTitle("Customer"+i);
+            customer.setTitle("Customer" + i);
             customers.add(customerService.saveCustomer(customer));
         }
-        
+
         List<Customer> loadedCustomers = new ArrayList<>();
         TextPageLink pageLink = new TextPageLink(23);
         TextPageData<Customer> pageData = null;
@@ -155,47 +155,47 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
                 pageLink = pageData.getNextPageLink();
             }
         } while (pageData.hasNext());
-        
+
         Collections.sort(customers, idComparator);
         Collections.sort(loadedCustomers, idComparator);
-        
+
         Assert.assertEquals(customers, loadedCustomers);
-        
+
         customerService.deleteCustomersByTenantId(tenantId);
 
         pageLink = new TextPageLink(33);
         pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
         Assert.assertFalse(pageData.hasNext());
         Assert.assertTrue(pageData.getData().isEmpty());
-        
+
         tenantService.deleteTenant(tenantId);
     }
-    
+
     @Test
     public void testFindCustomersByTenantIdAndTitle() {
         String title1 = "Customer title 1";
         List<Customer> customersTitle1 = new ArrayList<>();
-        for (int i=0;i<143;i++) {
+        for (int i = 0; i < 143; i++) {
             Customer customer = new Customer();
             customer.setTenantId(tenantId);
-            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
-            String title = title1+suffix;
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+            String title = title1 + suffix;
             title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
             customer.setTitle(title);
             customersTitle1.add(customerService.saveCustomer(customer));
         }
         String title2 = "Customer title 2";
         List<Customer> customersTitle2 = new ArrayList<>();
-        for (int i=0;i<175;i++) {
+        for (int i = 0; i < 175; i++) {
             Customer customer = new Customer();
             customer.setTenantId(tenantId);
-            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
-            String title = title2+suffix;
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+            String title = title2 + suffix;
             title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
             customer.setTitle(title);
             customersTitle2.add(customerService.saveCustomer(customer));
         }
-        
+
         List<Customer> loadedCustomersTitle1 = new ArrayList<>();
         TextPageLink pageLink = new TextPageLink(15, title1);
         TextPageData<Customer> pageData = null;
@@ -206,12 +206,12 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
                 pageLink = pageData.getNextPageLink();
             }
         } while (pageData.hasNext());
-        
+
         Collections.sort(customersTitle1, idComparator);
         Collections.sort(loadedCustomersTitle1, idComparator);
-        
+
         Assert.assertEquals(customersTitle1, loadedCustomersTitle1);
-        
+
         List<Customer> loadedCustomersTitle2 = new ArrayList<>();
         pageLink = new TextPageLink(4, title2);
         do {
@@ -224,22 +224,22 @@ public class CustomerServiceImplTest extends AbstractServiceTest {
 
         Collections.sort(customersTitle2, idComparator);
         Collections.sort(loadedCustomersTitle2, idComparator);
-        
+
         Assert.assertEquals(customersTitle2, loadedCustomersTitle2);
 
         for (Customer customer : loadedCustomersTitle1) {
             customerService.deleteCustomer(customer.getId());
         }
-        
+
         pageLink = new TextPageLink(4, title1);
         pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
         Assert.assertFalse(pageData.hasNext());
         Assert.assertEquals(0, pageData.getData().size());
-        
+
         for (Customer customer : loadedCustomersTitle2) {
             customerService.deleteCustomer(customer.getId());
         }
-        
+
         pageLink = new TextPageLink(4, title2);
         pageData = customerService.findCustomersByTenantId(tenantId, pageLink);
         Assert.assertFalse(pageData.hasNext());
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..376a88d
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java
@@ -0,0 +1,283 @@
+/**
+ * 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.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).get());
+
+        Assert.assertFalse(relationService.checkRelation(parentId, childId, "NOT_EXISTING_TYPE").get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, EntityRelation.CONTAINS_TYPE).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, parentId, "NOT_EXISTING_TYPE").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).get());
+
+        Assert.assertTrue(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).get());
+
+        Assert.assertTrue(relationService.deleteRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).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).get());
+
+        Assert.assertFalse(relationService.checkRelation(childId, subChildId, EntityRelation.CONTAINS_TYPE).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).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).get();
+        Assert.assertEquals(2, relations.size());
+
+        relations = relationService.findByFromAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFrom(parentB).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).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByFromAndType(parentB, EntityRelation.CONTAINS_TYPE).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).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();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(childB, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(1, relations.size());
+
+        relations = relationService.findByToAndType(parentA, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByToAndType(parentB, EntityRelation.MANAGES_TYPE).get();
+        Assert.assertEquals(0, relations.size());
+
+        relations = relationService.findByTo(childB).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));
+    }
+}
diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
index 79deaee..af03ac5 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDaoTest.java
@@ -71,6 +71,7 @@ public class JpaBaseEventDaoTest extends AbstractJpaDaoTest {
         String eventType = STATS;
         String eventUid = "be41c7a3-31f5-11e7-9cfd-2786e6aa2046";
         Event event = eventDao.findEvent(tenantId, new DeviceId(entityId), eventType, eventUid);
+        eventDao.find().stream().forEach(System.out::println);
         assertNotNull("Event expected to be not null", event);
         assertEquals("be41c7a2-31f5-11e7-9cfd-2786e6aa2046", event.getId().getId().toString());
     }
diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
index ae49cb1..779aa55 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
@@ -62,7 +62,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         saveEntries(deviceId, TS - 1);
         saveEntries(deviceId, TS);
 
-        ResultSetFuture rsFuture = tsService.findAllLatest(DataConstants.DEVICE, deviceId);
+        ResultSetFuture rsFuture = tsService.findAllLatest(deviceId);
         List<TsKvEntry> tsList = tsService.convertResultSetToTsKvEntryList(rsFuture.get());
 
         assertNotNull(tsList);
@@ -91,7 +91,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         saveEntries(deviceId, TS - 1);
         saveEntries(deviceId, TS);
 
-        List<ResultSet> rs = tsService.findLatest(DataConstants.DEVICE, deviceId, Collections.singleton(STRING_KEY)).get();
+        List<ResultSet> rs = tsService.findLatest(deviceId, Collections.singleton(STRING_KEY)).get();
         Assert.assertEquals(1, rs.size());
         Assert.assertEquals(toTsEntry(TS, stringKvEntry), tsService.convertResultToTsKvEntry(rs.get(0).one()));
     }
@@ -110,7 +110,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         entries.add(save(deviceId, 45000, 500));
         entries.add(save(deviceId, 55000, 600));
 
-        List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+        List<TsKvEntry> list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
                 60000, 20000, 3, Aggregation.NONE))).get();
         assertEquals(3, list.size());
         assertEquals(55000, list.get(0).getTs());
@@ -122,7 +122,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         assertEquals(35000, list.get(2).getTs());
         assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue());
 
-        list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+        list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
                 60000, 20000, 3, Aggregation.AVG))).get();
         assertEquals(3, list.size());
         assertEquals(10000, list.get(0).getTs());
@@ -134,7 +134,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         assertEquals(50000, list.get(2).getTs());
         assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue());
 
-        list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+        list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
                 60000, 20000, 3, Aggregation.SUM))).get();
 
         assertEquals(3, list.size());
@@ -147,7 +147,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         assertEquals(50000, list.get(2).getTs());
         assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue());
 
-        list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+        list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
                 60000, 20000, 3, Aggregation.MIN))).get();
 
         assertEquals(3, list.size());
@@ -160,7 +160,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         assertEquals(50000, list.get(2).getTs());
         assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue());
 
-        list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+        list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
                 60000, 20000, 3, Aggregation.MAX))).get();
 
         assertEquals(3, list.size());
@@ -173,7 +173,7 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
         assertEquals(50000, list.get(2).getTs());
         assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue());
 
-        list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
+        list = tsService.findAll(deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
                 60000, 20000, 3, Aggregation.COUNT))).get();
 
         assertEquals(3, list.size());
@@ -189,15 +189,15 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
 
     private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception {
         TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value));
-        tsService.save(DataConstants.DEVICE, deviceId, entry).get();
+        tsService.save(deviceId, entry).get();
         return entry;
     }
 
     private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException {
-        tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, stringKvEntry)).get();
-        tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, longKvEntry)).get();
-        tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, doubleKvEntry)).get();
-        tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, booleanKvEntry)).get();
+        tsService.save(deviceId, toTsEntry(ts, stringKvEntry)).get();
+        tsService.save(deviceId, toTsEntry(ts, longKvEntry)).get();
+        tsService.save(deviceId, toTsEntry(ts, doubleKvEntry)).get();
+        tsService.save(deviceId, toTsEntry(ts, booleanKvEntry)).get();
     }
 
     private static TsKvEntry toTsEntry(long ts, KvEntry entry) {
diff --git a/dao/src/test/resources/dbunit/events.xml b/dao/src/test/resources/dbunit/events.xml
index 2c02c19..943b45a 100644
--- a/dao/src/test/resources/dbunit/events.xml
+++ b/dao/src/test/resources/dbunit/events.xml
@@ -3,7 +3,7 @@
         id="be41c7a2-31f5-11e7-9cfd-2786e6aa2046"
         tenant_id="be41c7a0-31f5-11e7-9cfd-2786e6aa2046"
         entity_id="be41c7a1-31f5-11e7-9cfd-2786e6aa2046"
-        entity_type="1"
+        entity_type="7"
         event_type="STATS"
         event_uid="be41c7a3-31f5-11e7-9cfd-2786e6aa2046"
     />
@@ -11,7 +11,7 @@
         id="be41c7a4-31f5-11e7-9cfd-2786e6aa2046"
         tenant_id="be41c7a0-31f5-11e7-9cfd-2786e6aa2046"
         entity_id="be41c7a1-31f5-11e7-9cfd-2786e6aa2046"
-        entity_type="1"
+        entity_type="7"
         event_type="STATS"
         event_uid="be41c7a5-31f5-11e7-9cfd-2786e6aa2046"
     />
diff --git a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java
index 797ebf5..a07c80d 100644
--- a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java
+++ b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPlugin.java
@@ -55,6 +55,13 @@ public class RestApiCallPlugin extends AbstractPlugin<RestApiCallPluginConfigura
             this.headers.add(AUTHORIZATION_HEADER_NAME, String.format(AUTHORIZATION_HEADER_FORMAT, new String(token)));
         }
 
+        if (configuration.getHeaders() != null) {
+            configuration.getHeaders().forEach(h -> {
+                log.debug("Adding header to request object. Key = {}, Value = {}", h.getKey(), h.getValue());
+                this.headers.add(h.getKey(), h.getValue());
+            });
+        }
+
         init();
     }
 
diff --git a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java
index 2b20e9b..cfd23b8 100644
--- a/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java
+++ b/extensions/extension-rest-api-call/src/main/java/org/thingsboard/server/extensions/rest/plugin/RestApiCallPluginConfiguration.java
@@ -16,6 +16,9 @@
 package org.thingsboard.server.extensions.rest.plugin;
 
 import lombok.Data;
+import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties;
+
+import java.util.List;
 
 @Data
 public class RestApiCallPluginConfiguration {
@@ -27,4 +30,6 @@ public class RestApiCallPluginConfiguration {
 
     private String userName;
     private String password;
+
+    private List<KeyValuePluginProperties> headers;
 }
diff --git a/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json b/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json
index e0e4d18..06f8559 100644
--- a/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json
+++ b/extensions/extension-rest-api-call/src/main/resources/RestApiCallPluginDescriptor.json
@@ -30,6 +30,24 @@
       "password": {
         "title": "Password",
         "type": "string"
+      },
+      "headers": {
+        "title": "Request Headers",
+        "type": "array",
+        "items": {
+          "title": "Request Header",
+          "type": "object",
+          "properties": {
+            "key": {
+              "title": "Key",
+              "type": "string"
+            },
+            "value": {
+              "title": "Value",
+              "type": "string"
+            }
+          }
+        }
       }
     },
     "required": [
@@ -62,6 +80,7 @@
     {
       "key": "password",
       "type": "password"
-    }
+    },
+    "headers"
   ]
 }
\ No newline at end of file
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java
index 7b7d0ec..b1288d9 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java
@@ -19,4 +19,9 @@ package org.thingsboard.server.extensions.api.exception;
  * Created by ashvayka on 21.02.17.
  */
 public class UnauthorizedException extends Exception {
+
+    public UnauthorizedException(String message) {
+        super(message);
+    }
+
 }
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
index 988410d..2477ac3 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
@@ -49,7 +49,7 @@ public interface PluginContext {
         Device RPC API
      */
 
-    Optional<ServerAddress> resolve(DeviceId deviceId);
+    Optional<ServerAddress> resolve(EntityId entityId);
 
     void getDevice(DeviceId deviceId, PluginCallback<Device> pluginCallback);
 
@@ -77,33 +77,33 @@ public interface PluginContext {
      */
 
 
-    void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback);
+    void saveTsData(EntityId entityId, TsKvEntry entry, PluginCallback<Void> callback);
 
-    void saveTsData(DeviceId deviceId, List<TsKvEntry> entry, PluginCallback<Void> callback);
+    void saveTsData(EntityId entityId, List<TsKvEntry> entry, PluginCallback<Void> callback);
 
-    void loadTimeseries(DeviceId deviceId, List<TsKvQuery> queries, PluginCallback<List<TsKvEntry>> callback);
+    void loadTimeseries(EntityId entityId, List<TsKvQuery> queries, PluginCallback<List<TsKvEntry>> callback);
 
-    void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback);
+    void loadLatestTimeseries(EntityId entityId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback);
 
-    void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback);
+    void loadLatestTimeseries(EntityId entityId, PluginCallback<List<TsKvEntry>> callback);
 
     /*
         Attributes API
      */
 
-    void saveAttributes(TenantId tenantId, DeviceId deviceId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
+    void saveAttributes(TenantId tenantId, EntityId entityId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
 
-    void removeAttributes(TenantId tenantId, DeviceId deviceId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
+    void removeAttributes(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
 
-    void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, PluginCallback<Optional<AttributeKvEntry>> callback);
+    void loadAttribute(EntityId entityId, String attributeType, String attributeKey, PluginCallback<Optional<AttributeKvEntry>> callback);
 
-    void loadAttributes(DeviceId deviceId, String attributeType, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
+    void loadAttributes(EntityId entityId, String attributeType, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
 
-    void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback);
+    void loadAttributes(EntityId entityId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback);
 
-    void loadAttributes(DeviceId deviceId, Collection<String> attributeTypes, PluginCallback<List<AttributeKvEntry>> callback);
+    void loadAttributes(EntityId entityId, Collection<String> attributeTypes, PluginCallback<List<AttributeKvEntry>> callback);
 
-    void loadAttributes(DeviceId deviceId, Collection<String> attributeTypes, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
+    void loadAttributes(EntityId entityId, Collection<String> attributeTypes, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
 
     void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback);
 }
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index 9ccc087..ab5975f 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -42,6 +42,11 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.thingsboard.common</groupId>
+            <artifactId>transport</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
         </dependency>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
index 145f8c4..9555dd6 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
@@ -28,7 +28,8 @@ import lombok.NoArgsConstructor;
 public class GetHistoryCmd implements TelemetryPluginCmd {
 
     private int cmdId;
-    private String deviceId;
+    private String entityType;
+    private String entityId;
     private String keys;
     private long startTs;
     private long endTs;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
index 3574eae..0b69b89 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
@@ -26,7 +26,8 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
 public abstract class SubscriptionCmd implements TelemetryPluginCmd {
 
     private int cmdId;
-    private String deviceId;
+    private String entityType;
+    private String entityId;
     private String keys;
     private String scope;
     private boolean unsubscribe;
@@ -35,7 +36,7 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
 
     @Override
     public String toString() {
-        return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";
+        return "SubscriptionCmd [entityType=" + entityType  + ", entityId=" + entityId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";
     }
 
 }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.java
new file mode 100644
index 0000000..d7340c6
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryFeature.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.extensions.core.plugin.telemetry.handlers;
+
+/**
+ * Created by ashvayka on 08.05.17.
+ */
+public enum TelemetryFeature {
+
+    ATTRIBUTES, TIMESERIES;
+
+    public static TelemetryFeature forName(String name) {
+        return TelemetryFeature.valueOf(name.toUpperCase());
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
index 633f620..8864ae1 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -16,13 +16,19 @@
 package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.google.gson.JsonParser;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
 import org.thingsboard.server.extensions.api.plugins.PluginContext;
 import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler;
@@ -50,111 +56,94 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
     public void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
         RestRequest request = msg.getRequest();
         String[] pathParams = request.getPathParams();
-        if (pathParams.length >= 3) {
-            String deviceIdStr = pathParams[0];
-            String method = pathParams[1];
-            String entity = pathParams[2];
-            String scope = pathParams.length >= 4 ? pathParams[3] : null;
-            if (StringUtils.isEmpty(method) || StringUtils.isEmpty(entity) || StringUtils.isEmpty(deviceIdStr)) {
-                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
-                return;
-            }
+        if (pathParams.length < 4) {
+            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+            return;
+        }
 
-            DeviceId deviceId = DeviceId.fromString(deviceIdStr);
+        String entityType = pathParams[0];
+        String entityIdStr = pathParams[1];
+        String method = pathParams[2];
+        TelemetryFeature feature = TelemetryFeature.forName(pathParams[3]);
+        String scope = pathParams.length >= 5 ? pathParams[4] : null;
+        if (StringUtils.isEmpty(entityType) || EntityType.valueOf(entityType) == null || StringUtils.isEmpty(entityIdStr) || StringUtils.isEmpty(method) || StringUtils.isEmpty(feature)) {
+            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+            return;
+        }
 
-            if (method.equals("keys")) {
-                if (entity.equals("timeseries")) {
-                    ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
-                        @Override
-                        public void onSuccess(PluginContext ctx, List<TsKvEntry> value) {
-                            List<String> keys = value.stream().map(tsKv -> tsKv.getKey()).collect(Collectors.toList());
-                            msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
-                        }
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
 
-                        @Override
-                        public void onFailure(PluginContext ctx, Exception e) {
-                            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
-                        }
-                    });
-                } else if (entity.equals("attributes")) {
-                    PluginCallback<List<AttributeKvEntry>> callback = getAttributeKeysPluginCallback(msg);
-                    if (!StringUtils.isEmpty(scope)) {
-                        ctx.loadAttributes(deviceId, scope, callback);
-                    } else {
-                        ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
+        if (method.equals("keys")) {
+            if (feature == TelemetryFeature.TIMESERIES) {
+                ctx.loadLatestTimeseries(entityId, new PluginCallback<List<TsKvEntry>>() {
+                    @Override
+                    public void onSuccess(PluginContext ctx, List<TsKvEntry> value) {
+                        List<String> keys = value.stream().map(tsKv -> tsKv.getKey()).collect(Collectors.toList());
+                        msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+                    }
+
+                    @Override
+                    public void onFailure(PluginContext ctx, Exception e) {
+                        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
                     }
+                });
+            } else if (feature == TelemetryFeature.ATTRIBUTES) {
+                PluginCallback<List<AttributeKvEntry>> callback = getAttributeKeysPluginCallback(msg);
+                if (!StringUtils.isEmpty(scope)) {
+                    ctx.loadAttributes(entityId, scope, callback);
+                } else {
+                    ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
                 }
-            } else if (method.equals("values")) {
-                if ("timeseries".equals(entity)) {
-                    String keysStr = request.getParameter("keys");
-                    List<String> keys = Arrays.asList(keysStr.split(","));
+            }
+        } else if (method.equals("values")) {
+            if (feature == TelemetryFeature.TIMESERIES) {
+                String keysStr = request.getParameter("keys");
+                List<String> keys = Arrays.asList(keysStr.split(","));
 
-                    Optional<Long> startTs = request.getLongParamValue("startTs");
-                    Optional<Long> endTs = request.getLongParamValue("endTs");
-                    Optional<Long> interval = request.getLongParamValue("interval");
-                    Optional<Integer> limit = request.getIntParamValue("limit");
+                Optional<Long> startTs = request.getLongParamValue("startTs");
+                Optional<Long> endTs = request.getLongParamValue("endTs");
+                Optional<Long> interval = request.getLongParamValue("interval");
+                Optional<Integer> limit = request.getIntParamValue("limit");
 
-                    if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) {
-                        if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) {
-                            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
-                            return;
-                        }
-                        Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
+                if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) {
+                    if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) {
+                        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+                        return;
+                    }
+                    Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
 
-                        List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
-                                .collect(Collectors.toList());
-                        ctx.loadTimeseries(deviceId, queries, getTsKvListCallback(msg));
+                    List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
+                            .collect(Collectors.toList());
+                    ctx.loadTimeseries(entityId, queries, getTsKvListCallback(msg));
+                } else {
+                    ctx.loadLatestTimeseries(entityId, keys, getTsKvListCallback(msg));
+                }
+            } else if (feature == TelemetryFeature.ATTRIBUTES) {
+                String keys = request.getParameter("keys", "");
+
+                PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
+                if (!StringUtils.isEmpty(scope)) {
+                    if (!StringUtils.isEmpty(keys)) {
+                        List<String> keyList = Arrays.asList(keys.split(","));
+                        ctx.loadAttributes(entityId, scope, keyList, callback);
                     } else {
-                        ctx.loadLatestTimeseries(deviceId, keys, getTsKvListCallback(msg));
+                        ctx.loadAttributes(entityId, scope, callback);
                     }
-                } else if ("attributes".equals(entity)) {
-                    String keys = request.getParameter("keys", "");
-
-                    PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
-                    if (!StringUtils.isEmpty(scope)) {
-                        if (!StringUtils.isEmpty(keys)) {
-                            List<String> keyList = Arrays.asList(keys.split(","));
-                            ctx.loadAttributes(deviceId, scope, keyList, callback);
-                        } else {
-                            ctx.loadAttributes(deviceId, scope, callback);
-                        }
+                } else {
+                    if (!StringUtils.isEmpty(keys)) {
+                        List<String> keyList = Arrays.asList(keys.split(","));
+                        ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), keyList, callback);
                     } else {
-                        if (!StringUtils.isEmpty(keys)) {
-                            List<String> keyList = Arrays.asList(keys.split(","));
-                            ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), keyList, callback);
-                        } else {
-                            ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
-                        }
+                        ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
                     }
                 }
             }
-        } else {
-            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
         }
-    }
-
-    private PluginCallback<List<TsKvEntry>> getTsKvListCallback(final PluginRestMsg msg) {
-        return new PluginCallback<List<TsKvEntry>>() {
-            @Override
-            public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
-                Map<String, List<TsData>> result = new LinkedHashMap<>();
-                for (TsKvEntry entry : data) {
-                    List<TsData> vList = result.get(entry.getKey());
                     if (vList == null) {
                         vList = new ArrayList<>();
                         result.put(entry.getKey(), vList);
                     }
                     vList.add(new TsData(entry.getTs(), entry.getValueAsString()));
-                }
-                msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK));
-            }
-
-            @Override
-            public void onFailure(PluginContext ctx, Exception e) {
-                log.error("Failed to fetch historical data", e);
-                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
-            }
-        };
     }
 
     @Override
@@ -162,9 +151,26 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
         RestRequest request = msg.getRequest();
         try {
             String[] pathParams = request.getPathParams();
+            EntityId entityId;
+            String scope;
+            TelemetryFeature feature;
             if (pathParams.length == 2) {
-                DeviceId deviceId = DeviceId.fromString(pathParams[0]);
-                String scope = pathParams[1];
+                entityId = DeviceId.fromString(pathParams[0]);
+                scope = pathParams[1];
+                feature = TelemetryFeature.ATTRIBUTES;
+            } else if (pathParams.length == 3) {
+                entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]);
+                scope = pathParams[2];
+                feature = TelemetryFeature.ATTRIBUTES;
+            } else if (pathParams.length == 4) {
+                entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]);
+                feature = TelemetryFeature.forName(pathParams[2].toUpperCase());
+                scope = pathParams[3];
+            } else {
+                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+                return;
+            }
+            if (feature == TelemetryFeature.ATTRIBUTES) {
                 if (DataConstants.SERVER_SCOPE.equals(scope) ||
                         DataConstants.SHARED_SCOPE.equals(scope)) {
                     JsonNode jsonNode = jsonMapper.readTree(request.getRequestBody());
@@ -185,11 +191,11 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
                             }
                         });
                         if (attributes.size() > 0) {
-                            ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, attributes, new PluginCallback<Void>() {
+                            ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), entityId, scope, attributes, new PluginCallback<Void>() {
                                 @Override
                                 public void onSuccess(PluginContext ctx, Void value) {
                                     msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
-                                    subscriptionManager.onAttributesUpdateFromServer(ctx, deviceId, scope, attributes);
+                                    subscriptionManager.onAttributesUpdateFromServer(ctx, entityId, scope, attributes);
                                 }
 
                                 @Override
@@ -202,6 +208,28 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
                         }
                     }
                 }
+            } else if (feature == TelemetryFeature.TIMESERIES) {
+                TelemetryUploadRequest telemetryRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(request.getRequestBody()));
+                List<TsKvEntry> entries = new ArrayList<>();
+                for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) {
+                    for (KvEntry kv : entry.getValue()) {
+                        entries.add(new BasicTsKvEntry(entry.getKey(), kv));
+                    }
+                }
+                ctx.saveTsData(entityId, entries, new PluginCallback<Void>() {
+                    @Override
+                    public void onSuccess(PluginContext ctx, Void value) {
+                        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+                        subscriptionManager.onTimeseriesUpdateFromServer(ctx, entityId, entries);
+                    }
+
+                    @Override
+                    public void onFailure(PluginContext ctx, Exception e) {
+                        log.error("Failed to save attributes", e);
+                        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+                    }
+                });
+                return;
             }
         } catch (IOException | RuntimeException e) {
             log.debug("Failed to process POST request due to exception", e);
@@ -214,29 +242,38 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
         RestRequest request = msg.getRequest();
         try {
             String[] pathParams = request.getPathParams();
+            EntityId entityId;
+            String scope;
             if (pathParams.length == 2) {
-                DeviceId deviceId = DeviceId.fromString(pathParams[0]);
-                String scope = pathParams[1];
-                if (DataConstants.SERVER_SCOPE.equals(scope) ||
-                        DataConstants.SHARED_SCOPE.equals(scope) ||
-                        DataConstants.CLIENT_SCOPE.equals(scope)) {
-                    String keysParam = request.getParameter("keys");
-                    if (!StringUtils.isEmpty(keysParam)) {
-                        String[] keys = keysParam.split(",");
-                        ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
-                            @Override
-                            public void onSuccess(PluginContext ctx, Void value) {
-                                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
-                            }
+                entityId = DeviceId.fromString(pathParams[0]);
+                scope = pathParams[1];
+            } else if (pathParams.length == 3) {
+                entityId = EntityIdFactory.getByTypeAndId(pathParams[0], pathParams[1]);
+                scope = pathParams[2];
+            } else {
+                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+                return;
+            }
 
-                            @Override
-                            public void onFailure(PluginContext ctx, Exception e) {
-                                log.error("Failed to remove attributes", e);
-                                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
-                            }
-                        });
-                        return;
-                    }
+            if (DataConstants.SERVER_SCOPE.equals(scope) ||
+                    DataConstants.SHARED_SCOPE.equals(scope) ||
+                    DataConstants.CLIENT_SCOPE.equals(scope)) {
+                String keysParam = request.getParameter("keys");
+                if (!StringUtils.isEmpty(keysParam)) {
+                    String[] keys = keysParam.split(",");
+                    ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), entityId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
+                        @Override
+                        public void onSuccess(PluginContext ctx, Void value) {
+                            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+                        }
+
+                        @Override
+                        public void onFailure(PluginContext ctx, Exception e) {
+                            log.error("Failed to remove attributes", e);
+                            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+                        }
+                    });
+                    return;
                 }
             }
         } catch (RuntimeException e) {
@@ -278,4 +315,29 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
             }
         };
     }
+
+
+    private PluginCallback<List<TsKvEntry>> getTsKvListCallback(final PluginRestMsg msg) {
+        return new PluginCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
+                Map<String, List<TsData>> result = new LinkedHashMap<>();
+                for (TsKvEntry entry : data) {
+                    List<TsData> vList = result.get(entry.getKey());
+                    if (vList == null) {
+                        vList = new ArrayList<>();
+                        result.put(entry.getKey(), vList);
+                    }
+                    vList.add(new TsData(entry.getTs(), entry.getValueAsString()));
+                }
+                msg.getResponseHolder().setResult(new ResponseEntity<>(result, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(PluginContext ctx, Exception e) {
+                log.error("Failed to fetch historical data", e);
+                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+            }
+        };
+    }
 }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
index a0fc9fc..a1b734e 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
@@ -18,7 +18,8 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
 import com.google.protobuf.InvalidProtocolBufferException;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.kv.*;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.extensions.api.plugins.PluginContext;
@@ -44,9 +45,10 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
 
     private static final int SUBSCRIPTION_CLAZZ = 1;
     private static final int ATTRIBUTES_UPDATE_CLAZZ = 2;
-    private static final int SUBSCRIPTION_UPDATE_CLAZZ = 3;
-    private static final int SESSION_CLOSE_CLAZZ = 4;
-    private static final int SUBSCRIPTION_CLOSE_CLAZZ = 5;
+    private static final int TIMESERIES_UPDATE_CLAZZ = 3;
+    private static final int SUBSCRIPTION_UPDATE_CLAZZ = 4;
+    private static final int SESSION_CLOSE_CLAZZ = 5;
+    private static final int SUBSCRIPTION_CLOSE_CLAZZ = 6;
 
     @Override
     public void process(PluginContext ctx, RpcMsg msg) {
@@ -60,6 +62,9 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
             case ATTRIBUTES_UPDATE_CLAZZ:
                 processAttributeUpdate(ctx, msg);
                 break;
+            case TIMESERIES_UPDATE_CLAZZ:
+                processTimeseriesUpdate(ctx, msg);
+                break;
             case SESSION_CLOSE_CLAZZ:
                 processSessionClose(ctx, msg);
                 break;
@@ -88,10 +93,21 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         } catch (InvalidProtocolBufferException e) {
             throw new RuntimeException(e);
         }
-        subscriptionManager.onAttributesUpdateFromServer(ctx, DeviceId.fromString(proto.getDeviceId()), proto.getScope(),
+        subscriptionManager.onAttributesUpdateFromServer(ctx, EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), proto.getScope(),
                 proto.getDataList().stream().map(this::toAttribute).collect(Collectors.toList()));
     }
 
+    private void processTimeseriesUpdate(PluginContext ctx, RpcMsg msg) {
+        TimeseriesUpdateProto proto;
+        try {
+            proto = TimeseriesUpdateProto.parseFrom(msg.getMsgData());
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException(e);
+        }
+        subscriptionManager.onTimeseriesUpdateFromServer(ctx, EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()),
+                proto.getDataList().stream().map(this::toTimeseries).collect(Collectors.toList()));
+    }
+
     private void processSubscriptionCmd(PluginContext ctx, RpcMsg msg) {
         SubscriptionProto proto;
         try {
@@ -101,7 +117,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         }
         Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs));
         Subscription subscription = new Subscription(
-                new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), DeviceId.fromString(proto.getDeviceId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap),
+                new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap),
                 false, msg.getServerAddress());
         subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription);
     }
@@ -110,7 +126,8 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         SubscriptionProto.Builder builder = SubscriptionProto.newBuilder();
         builder.setSessionId(sessionId);
         builder.setSubscriptionId(cmd.getSubscriptionId());
-        builder.setDeviceId(cmd.getDeviceId().toString());
+        builder.setEntityType(cmd.getEntityId().getEntityType().name());
+        builder.setEntityId(cmd.getEntityId().getId().toString());
         builder.setType(cmd.getType().name());
         builder.setAllKeys(cmd.isAllKeys());
         cmd.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
@@ -195,41 +212,62 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         }
     }
 
-    public void onAttributesUpdate(PluginContext ctx, ServerAddress address, DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
-        ctx.sendPluginRpcMsg(new RpcMsg(address, ATTRIBUTES_UPDATE_CLAZZ, getAttributesUpdateProto(deviceId, scope, attributes).toByteArray()));
+    public void onAttributesUpdate(PluginContext ctx, ServerAddress address, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+        ctx.sendPluginRpcMsg(new RpcMsg(address, ATTRIBUTES_UPDATE_CLAZZ, getAttributesUpdateProto(entityId, scope, attributes).toByteArray()));
     }
 
-    private AttributeUpdateProto getAttributesUpdateProto(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
+    public void onTimeseriesUpdate(PluginContext ctx, ServerAddress address, EntityId entityId, List<TsKvEntry> tsKvEntries) {
+        ctx.sendPluginRpcMsg(new RpcMsg(address, TIMESERIES_UPDATE_CLAZZ, getTimeseriesUpdateProto(entityId, tsKvEntries).toByteArray()));
+    }
+
+    private TimeseriesUpdateProto getTimeseriesUpdateProto(EntityId entityId, List<TsKvEntry> tsKvEntries) {
+        TimeseriesUpdateProto.Builder builder = TimeseriesUpdateProto.newBuilder();
+        builder.setEntityId(entityId.getId().toString());
+        builder.setEntityType(entityId.getEntityType().name());
+        tsKvEntries.forEach(attr -> builder.addData(toKeyValueProto(attr.getTs(), attr).build()));
+        return builder.build();
+    }
+
+    private AttributeUpdateProto getAttributesUpdateProto(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
         AttributeUpdateProto.Builder builder = AttributeUpdateProto.newBuilder();
-        builder.setDeviceId(deviceId.toString());
+        builder.setEntityId(entityId.getId().toString());
+        builder.setEntityType(entityId.getEntityType().name());
         builder.setScope(scope);
-        attributes.forEach(
-                attr -> {
-                    AttributeUpdateValueListProto.Builder dataBuilder = AttributeUpdateValueListProto.newBuilder();
-                    dataBuilder.setKey(attr.getKey());
-                    dataBuilder.setTs(attr.getLastUpdateTs());
-                    dataBuilder.setValueType(attr.getDataType().ordinal());
-                    switch (attr.getDataType()) {
-                        case BOOLEAN:
-                            dataBuilder.setBoolValue(attr.getBooleanValue().get());
-                            break;
-                        case LONG:
-                            dataBuilder.setLongValue(attr.getLongValue().get());
-                            break;
-                        case DOUBLE:
-                            dataBuilder.setDoubleValue(attr.getDoubleValue().get());
-                            break;
-                        case STRING:
-                            dataBuilder.setStrValue(attr.getStrValue().get());
-                            break;
-                    }
-                    builder.addData(dataBuilder.build());
-                }
-        );
+        attributes.forEach(attr -> builder.addData(toKeyValueProto(attr.getLastUpdateTs(), attr).build()));
         return builder.build();
     }
 
-    private AttributeKvEntry toAttribute(AttributeUpdateValueListProto proto) {
+    private KeyValueProto.Builder toKeyValueProto(long ts, KvEntry attr) {
+        KeyValueProto.Builder dataBuilder = KeyValueProto.newBuilder();
+        dataBuilder.setKey(attr.getKey());
+        dataBuilder.setTs(ts);
+        dataBuilder.setValueType(attr.getDataType().ordinal());
+        switch (attr.getDataType()) {
+            case BOOLEAN:
+                dataBuilder.setBoolValue(attr.getBooleanValue().get());
+                break;
+            case LONG:
+                dataBuilder.setLongValue(attr.getLongValue().get());
+                break;
+            case DOUBLE:
+                dataBuilder.setDoubleValue(attr.getDoubleValue().get());
+                break;
+            case STRING:
+                dataBuilder.setStrValue(attr.getStrValue().get());
+                break;
+        }
+        return dataBuilder;
+    }
+
+    private AttributeKvEntry toAttribute(KeyValueProto proto) {
+        return new BaseAttributeKvEntry(getKvEntry(proto), proto.getTs());
+    }
+
+    private TsKvEntry toTimeseries(KeyValueProto proto) {
+        return new BasicTsKvEntry(proto.getTs(), getKvEntry(proto));
+    }
+
+    private KvEntry getKvEntry(KeyValueProto proto) {
         KvEntry entry = null;
         DataType type = DataType.values()[proto.getValueType()];
         switch (type) {
@@ -246,7 +284,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
                 entry = new StringDataEntry(proto.getKey(), proto.getStrValue());
                 break;
         }
-        return new BaseAttributeKvEntry(entry, proto.getTs());
+        return entry;
     }
 
 }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
index f018aaa..c231ffa 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
@@ -19,7 +19,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.DataConstants;
-import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
 import org.thingsboard.server.common.data.kv.*;
 import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
@@ -101,8 +102,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
             if (cmd.isUnsubscribe()) {
                 unsubscribe(ctx, cmd, sessionId);
             } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
-                log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
-                DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+                EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+                log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
                 Optional<Set<String>> keysOptional = getKeys(cmd);
                 SubscriptionState sub;
                 if (keysOptional.isPresent()) {
@@ -118,8 +119,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                             keys.forEach(key -> subState.put(key, 0L));
                             attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
 
-                            SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
-                            subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+                            SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState);
+                            subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
                         }
 
                         @Override
@@ -138,9 +139,9 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                     };
 
                     if (StringUtils.isEmpty(cmd.getScope())) {
-                        ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), keys, callback);
+                        ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), keys, callback);
                     } else {
-                        ctx.loadAttributes(deviceId, cmd.getScope(), keys, callback);
+                        ctx.loadAttributes(entityId, cmd.getScope(), keys, callback);
                     }
                 } else {
                     PluginCallback<List<AttributeKvEntry>> callback = new PluginCallback<List<AttributeKvEntry>>() {
@@ -152,8 +153,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                             Map<String, Long> subState = new HashMap<>(attributesData.size());
                             attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
 
-                            SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
-                            subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+                            SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState);
+                            subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
                         }
 
                         @Override
@@ -166,9 +167,9 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                     };
 
                     if (StringUtils.isEmpty(cmd.getScope())) {
-                        ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
+                        ctx.loadAttributes(entityId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
                     } else {
-                        ctx.loadAttributes(deviceId, cmd.getScope(), callback);
+                        ctx.loadAttributes(entityId, cmd.getScope(), callback);
                     }
                 }
             }
@@ -183,33 +184,33 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
             if (cmd.isUnsubscribe()) {
                 unsubscribe(ctx, cmd, sessionId);
             } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
-                DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+                EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
                 Optional<Set<String>> keysOptional = getKeys(cmd);
 
                 if (keysOptional.isPresent()) {
                     long startTs;
                     if (cmd.getTimeWindow() > 0) {
                         List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
-                        log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
+                        log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId);
                         startTs = cmd.getStartTs();
                         long endTs = cmd.getStartTs() + cmd.getTimeWindow();
                         List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
-                        ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
+                        ctx.loadTimeseries(entityId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys));
                     } else {
                         List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
                         startTs = System.currentTimeMillis();
-                        log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
-                        ctx.loadLatestTimeseries(deviceId, keys, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
+                        log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId);
+                        ctx.loadLatestTimeseries(entityId, keys, getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys));
                     }
                 } else {
-                    ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
+                    ctx.loadLatestTimeseries(entityId, new PluginCallback<List<TsKvEntry>>() {
                         @Override
                         public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
                             sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
                             Map<String, Long> subState = new HashMap<>(data.size());
                             data.forEach(v -> subState.put(v.getKey(), v.getTs()));
-                            SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, true, subState);
-                            subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+                            SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState);
+                            subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
                         }
 
                         @Override
@@ -230,7 +231,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
         }
     }
 
-    private PluginCallback<List<TsKvEntry>> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final DeviceId deviceId, final long startTs, final List<String> keys) {
+    private PluginCallback<List<TsKvEntry>> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List<String> keys) {
         return new PluginCallback<List<TsKvEntry>>() {
             @Override
             public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
@@ -239,8 +240,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                 Map<String, Long> subState = new HashMap<>(keys.size());
                 keys.forEach(key -> subState.put(key, startTs));
                 data.forEach(v -> subState.put(v.getKey(), v.getTs()));
-                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
-                subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState);
+                subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
             }
 
             @Override
@@ -263,7 +264,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
             sendWsMsg(ctx, sessionRef, update);
             return;
         }
-        if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
             SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
                     "Device id is empty!");
             sendWsMsg(ctx, sessionRef, update);
@@ -275,10 +276,11 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
             sendWsMsg(ctx, sessionRef, update);
             return;
         }
-        DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+        EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
         List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
-        List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
-        ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
+        List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg())))
+                .collect(Collectors.toList());
+        ctx.loadTimeseries(entityId, queries, new PluginCallback<List<TsKvEntry>>() {
             @Override
             public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
                 sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
@@ -321,7 +323,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
     }
 
     private void unsubscribe(PluginContext ctx, SubscriptionCmd cmd, String sessionId) {
-        if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
             cleanupWebSocketSession(ctx, sessionId);
         } else {
             subscriptionManager.removeSubscription(ctx, sessionId, cmd.getCmdId());
@@ -329,7 +331,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
     }
 
     private boolean validateSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd) {
-        if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
             SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
                     "Device id is empty!");
             sendWsMsg(ctx, sessionRef, update);
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
index 430a559..1285cfa 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 
 import java.util.Map;
@@ -42,8 +43,8 @@ public class Subscription {
         return getSub().getSubscriptionId();
     }
 
-    public DeviceId getDeviceId() {
-        return getSub().getDeviceId();
+    public EntityId getEntityId() {
+        return getSub().getEntityId();
     }
 
     public SubscriptionType getType() {
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
index fbadd64..5e15fda 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
@@ -16,9 +16,8 @@
 package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
 
 import lombok.AllArgsConstructor;
-import lombok.Data;
 import lombok.Getter;
-import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
 
 import java.util.Map;
 
@@ -30,7 +29,7 @@ public class SubscriptionState {
 
     @Getter private final String wsSessionId;
     @Getter private final int subscriptionId;
-    @Getter private final DeviceId deviceId;
+    @Getter private final EntityId entityId;
     @Getter private final SubscriptionType type;
     @Getter private final boolean allKeys;
     @Getter private final Map<String, Long> keyStates;
@@ -44,7 +43,7 @@ public class SubscriptionState {
 
         if (subscriptionId != that.subscriptionId) return false;
         if (wsSessionId != null ? !wsSessionId.equals(that.wsSessionId) : that.wsSessionId != null) return false;
-        if (deviceId != null ? !deviceId.equals(that.deviceId) : that.deviceId != null) return false;
+        if (entityId != null ? !entityId.equals(that.entityId) : that.entityId != null) return false;
         return type == that.type;
     }
 
@@ -52,7 +51,7 @@ public class SubscriptionState {
     public int hashCode() {
         int result = wsSessionId != null ? wsSessionId.hashCode() : 0;
         result = 31 * result + subscriptionId;
-        result = 31 * result + (deviceId != null ? deviceId.hashCode() : 0);
+        result = 31 * result + (entityId != null ? entityId.hashCode() : 0);
         result = 31 * result + (type != null ? type.hashCode() : 0);
         return result;
     }
@@ -61,7 +60,7 @@ public class SubscriptionState {
     public String toString() {
         return "SubscriptionState{" +
                 "type=" + type +
-                ", deviceId=" + deviceId +
+                ", entityId=" + entityId +
                 ", subscriptionId=" + subscriptionId +
                 ", wsSessionId='" + wsSessionId + '\'' +
                 '}';
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
index 8cb1bf3..60f46ce 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
@@ -19,6 +19,7 @@ import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.kv.*;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
@@ -39,7 +40,7 @@ import java.util.function.Function;
 @Slf4j
 public class SubscriptionManager {
 
-    private final Map<DeviceId, Set<Subscription>> subscriptionsByDeviceId = new HashMap<>();
+    private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>();
 
     private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>();
 
@@ -48,28 +49,28 @@ public class SubscriptionManager {
     @Setter
     private TelemetryRpcMsgHandler rpcHandler;
 
-    public void addLocalWsSubscription(PluginContext ctx, String sessionId, DeviceId deviceId, SubscriptionState sub) {
-        Optional<ServerAddress> server = ctx.resolve(deviceId);
+    public void addLocalWsSubscription(PluginContext ctx, String sessionId, EntityId entityId, SubscriptionState sub) {
+        Optional<ServerAddress> server = ctx.resolve(entityId);
         Subscription subscription;
         if (server.isPresent()) {
             ServerAddress address = server.get();
-            log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), deviceId, address);
+            log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address);
             subscription = new Subscription(sub, true, address);
             rpcHandler.onNewSubscription(ctx, address, sessionId, subscription);
         } else {
-            log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), deviceId);
+            log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId);
             subscription = new Subscription(sub, true);
         }
-        registerSubscription(sessionId, deviceId, subscription);
+        registerSubscription(sessionId, entityId, subscription);
     }
 
     public void addRemoteWsSubscription(PluginContext ctx, ServerAddress address, String sessionId, Subscription subscription) {
-        DeviceId deviceId = subscription.getDeviceId();
-        log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), deviceId, address);
-        registerSubscription(sessionId, deviceId, subscription);
+        EntityId entityId = subscription.getEntityId();
+        log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), entityId, address);
+        registerSubscription(sessionId, entityId, subscription);
         if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
             final Map<String, Long> keyStates = subscription.getKeyStates();
-            ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
+            ctx.loadAttributes(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
                 @Override
                 public void onSuccess(PluginContext ctx, List<AttributeKvEntry> values) {
                     List<TsKvEntry> missedUpdates = new ArrayList<>();
@@ -95,7 +96,7 @@ public class SubscriptionManager {
                 queries.add(new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs));
             });
 
-            ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
+            ctx.loadTimeseries(entityId, queries, new PluginCallback<List<TsKvEntry>>() {
                 @Override
                 public void onSuccess(PluginContext ctx, List<TsKvEntry> missedUpdates) {
                     if (!missedUpdates.isEmpty()) {
@@ -112,11 +113,11 @@ public class SubscriptionManager {
 
     }
 
-    private void registerSubscription(String sessionId, DeviceId deviceId, Subscription subscription) {
-        Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(subscription.getDeviceId());
+    private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(subscription.getEntityId());
         if (deviceSubscriptions == null) {
             deviceSubscriptions = new HashSet<>();
-            subscriptionsByDeviceId.put(deviceId, deviceSubscriptions);
+            subscriptionsByEntityId.put(entityId, deviceSubscriptions);
         }
         deviceSubscriptions.add(subscription);
         Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
@@ -133,7 +134,7 @@ public class SubscriptionManager {
         if (sessionSubscriptions != null) {
             Subscription subscription = sessionSubscriptions.remove(subscriptionId);
             if (subscription != null) {
-                DeviceId deviceId = subscription.getDeviceId();
+                EntityId entityId = subscription.getEntityId();
                 if (subscription.isLocal() && subscription.getServer() != null) {
                     rpcHandler.onSubscriptionClose(ctx, subscription.getServer(), sessionId, subscription.getSubscriptionId());
                 }
@@ -143,13 +144,13 @@ public class SubscriptionManager {
                 } else {
                     log.debug("[{}] Removed session subscription.", sessionId);
                 }
-                Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+                Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
                 if (deviceSubscriptions != null) {
                     boolean result = deviceSubscriptions.remove(subscription);
                     if (result) {
                         if (deviceSubscriptions.size() == 0) {
                             log.debug("[{}] Removed last subscription for particular device.", sessionId);
-                            subscriptionsByDeviceId.remove(deviceId);
+                            subscriptionsByEntityId.remove(entityId);
                         } else {
                             log.debug("[{}] Removed device subscription.", sessionId);
                         }
@@ -167,8 +168,8 @@ public class SubscriptionManager {
         }
     }
 
-    public void onLocalSubscriptionUpdate(PluginContext ctx, DeviceId deviceId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
-        Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+    public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
         if (deviceSubscriptions != null) {
             deviceSubscriptions.stream().filter(s -> type == s.getType()).forEach(s -> {
                 String sessionId = s.getWsSessionId();
@@ -184,7 +185,7 @@ public class SubscriptionManager {
                 }
             });
         } else {
-            log.debug("[{}] No device subscriptions to process!", deviceId);
+            log.debug("[{}] No device subscriptions to process!", entityId);
         }
     }
 
@@ -197,10 +198,10 @@ public class SubscriptionManager {
         }
     }
 
-    public void onAttributesUpdateFromServer(PluginContext ctx, DeviceId deviceId, String scope, List<AttributeKvEntry> attributes) {
-        Optional<ServerAddress> serverAddress = ctx.resolve(deviceId);
+    public void onAttributesUpdateFromServer(PluginContext ctx, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+        Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
         if (!serverAddress.isPresent()) {
-            onLocalSubscriptionUpdate(ctx, deviceId, SubscriptionType.ATTRIBUTES, s -> {
+            onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.ATTRIBUTES, s -> {
                 List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
                 for (AttributeKvEntry kv : attributes) {
                     if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
@@ -210,7 +211,24 @@ public class SubscriptionManager {
                 return subscriptionUpdate;
             });
         } else {
-            rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), deviceId, scope, attributes);
+            rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), entityId, scope, attributes);
+        }
+    }
+
+    public void onTimeseriesUpdateFromServer(PluginContext ctx, EntityId entityId, List<TsKvEntry> entries) {
+        Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
+        if (!serverAddress.isPresent()) {
+            onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.TIMESERIES, s -> {
+                List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
+                for (TsKvEntry kv : entries) {
+                    if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
+                        subscriptionUpdate.add(kv);
+                    }
+                }
+                return subscriptionUpdate;
+            });
+        } else {
+            rpcHandler.onTimeseriesUpdate(ctx, serverAddress.get(), entityId, entries);
         }
     }
 
@@ -243,11 +261,11 @@ public class SubscriptionManager {
             int sessionSubscriptionSize = sessionSubscriptions.size();
 
             for (Subscription subscription : sessionSubscriptions.values()) {
-                DeviceId deviceId = subscription.getDeviceId();
-                Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+                EntityId entityId = subscription.getEntityId();
+                Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
                 deviceSubscriptions.remove(subscription);
                 if (deviceSubscriptions.isEmpty()) {
-                    subscriptionsByDeviceId.remove(deviceId);
+                    subscriptionsByEntityId.remove(entityId);
                 }
             }
             subscriptionsByWsSessionId.remove(sessionId);
@@ -272,9 +290,9 @@ public class SubscriptionManager {
 
     public void onClusterUpdate(PluginContext ctx) {
         log.trace("Processing cluster onUpdate msg!");
-        Iterator<Map.Entry<DeviceId, Set<Subscription>>> deviceIterator = subscriptionsByDeviceId.entrySet().iterator();
+        Iterator<Map.Entry<EntityId, Set<Subscription>>> deviceIterator = subscriptionsByEntityId.entrySet().iterator();
         while (deviceIterator.hasNext()) {
-            Map.Entry<DeviceId, Set<Subscription>> e = deviceIterator.next();
+            Map.Entry<EntityId, Set<Subscription>> e = deviceIterator.next();
             Set<Subscription> subscriptions = e.getValue();
             Optional<ServerAddress> newAddressOptional = ctx.resolve(e.getKey());
             if (newAddressOptional.isPresent()) {
@@ -317,6 +335,6 @@ public class SubscriptionManager {
 
     public void clear() {
         subscriptionsByWsSessionId.clear();
-        subscriptionsByDeviceId.clear();
+        subscriptionsByEntityId.clear();
     }
 }
\ No newline at end of file
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
index 4e7793e..ac168d0 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
@@ -30,7 +30,7 @@ import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
 import org.thingsboard.server.extensions.api.rules.RuleException;
 import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction;
 
-import java.time.LocalDateTime;
+import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 
 /**
@@ -51,7 +51,7 @@ public class TimePlugin extends AbstractPlugin<TimePluginConfiguration> implemen
 
             String reply;
             if (!StringUtils.isEmpty(format)) {
-                reply = "\"" + formatter.format(LocalDateTime.now()) + "\"";
+                reply = "\"" + formatter.format(ZonedDateTime.now()) + "\"";
             } else {
                 reply = Long.toString(System.currentTimeMillis());
             }
diff --git a/extensions-core/src/main/proto/telemetry.proto b/extensions-core/src/main/proto/telemetry.proto
index be97b4f..2bfef59 100644
--- a/extensions-core/src/main/proto/telemetry.proto
+++ b/extensions-core/src/main/proto/telemetry.proto
@@ -22,10 +22,11 @@ option java_outer_classname = "TelemetryPluginProtos";
 message SubscriptionProto {
   string sessionId = 1;
   int32 subscriptionId = 2;
-  string deviceId = 3;
-  string type = 4;
-  bool allKeys = 5;
-  repeated SubscriptionKetStateProto keyStates = 6;
+  string entityType = 3;
+  string entityId = 4;
+  string type = 5;
+  bool allKeys = 6;
+  repeated SubscriptionKetStateProto keyStates = 7;
 }
 
 message SubscriptionUpdateProto {
@@ -37,9 +38,16 @@ message SubscriptionUpdateProto {
 }
 
 message AttributeUpdateProto {
-    string deviceId = 1;
-    string scope = 2;
-    repeated AttributeUpdateValueListProto data = 3;
+    string entityType = 1;
+    string entityId = 2;
+    string scope = 3;
+    repeated KeyValueProto data = 4;
+}
+
+message TimeseriesUpdateProto {
+    string entityType = 1;
+    string entityId = 2;
+    repeated KeyValueProto data = 4;
 }
 
 message SessionCloseProto {
@@ -62,7 +70,7 @@ message SubscriptionUpdateValueListProto {
     repeated string value = 3;
 }
 
-message AttributeUpdateValueListProto {
+message KeyValueProto {
     string key = 1;
     int64 ts = 2;
     int32 valueType = 3;

tools/pom.xml 36(+0 -36)

diff --git a/tools/pom.xml b/tools/pom.xml
index 3e76f46..96235e2 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -54,40 +54,4 @@
         </dependency>
     </dependencies>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-shade-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>shade</goal>
-                        </goals>
-                        <configuration>
-                            <filters>
-                                <filter>
-                                    <artifact>*:*</artifact>
-                                    <excludes>
-                                        <exclude>META-INF/*.SF</exclude>
-                                        <exclude>META-INF/*.DSA</exclude>
-                                        <exclude>META-INF/*.RSA</exclude>
-                                    </excludes>
-                                </filter>
-                            </filters>
-                            <transformers>
-                                <transformer
-                                    implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
-                                    <manifestEntries>
-                                        <Main-Class>org.thingsboard.client.tools.MqttStressTestTool</Main-Class>
-                                    </manifestEntries>
-                                </transformer>
-                            </transformers>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
 </project>
diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
index f742e61..d635dbc 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
@@ -27,6 +27,7 @@ import org.springframework.http.client.support.HttpRequestWrapper;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestTemplate;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 
@@ -76,6 +77,12 @@ public class RestClient implements ClientHttpRequestInterceptor {
         return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
     }
 
+
+    public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
+        return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
+                customerId.toString(), deviceId.toString()).getBody();
+    }
+
     public DeviceCredentials getCredentials(DeviceId id) {
         return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
     }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 76cdf1a..36868c5 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -247,7 +247,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
 
     private MqttMessage createUnSubAckMessage(int msgId) {
         MqttFixedHeader mqttFixedHeader =
-                new MqttFixedHeader(SUBACK, false, AT_LEAST_ONCE, false, 0);
+                new MqttFixedHeader(UNSUBACK, false, AT_LEAST_ONCE, false, 0);
         MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
         return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader);
     }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
index 9444cdd..af109f6 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
@@ -40,6 +40,8 @@ import java.security.cert.CertificateException;
  */
 public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
 
+    private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
+
     private final SessionMsgProcessor processor;
     private final DeviceService deviceService;
     private final DeviceAuthService authService;
@@ -63,7 +65,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
             sslHandler = sslHandlerProvider.getSslHandler();
             pipeline.addLast(sslHandler);
         }
-        pipeline.addLast("decoder", new MqttDecoder());
+        pipeline.addLast("decoder", new MqttDecoder(MAX_PAYLOAD_SIZE));
         pipeline.addLast("encoder", MqttEncoder.INSTANCE);
 
         MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, adaptor, sslHandler);
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index c7deeec..a78319a 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -82,6 +82,7 @@ public class GatewaySessionCtx {
             });
             GatewayDeviceSessionCtx ctx = new GatewayDeviceSessionCtx(this, device);
             devices.put(deviceName, ctx);
+            log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName);
             processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
             processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
         }
@@ -94,6 +95,9 @@ public class GatewaySessionCtx {
         if (deviceSessionCtx != null) {
             processor.process(SessionCloseMsg.onDisconnect(deviceSessionCtx.getSessionId()));
             deviceSessionCtx.setClosed(true);
+            log.debug("[{}] Removed device [{}] from the gateway session", gatewaySessionId, deviceName);
+        } else {
+            log.debug("[{}] Device [{}] was already removed from the gateway session", gatewaySessionId, deviceName);
         }
         ack(msg);
     }
@@ -134,7 +138,7 @@ public class GatewaySessionCtx {
             JsonObject jsonObj = json.getAsJsonObject();
             String deviceName = checkDeviceConnected(jsonObj.get("device").getAsString());
             Integer requestId = jsonObj.get("id").getAsInt();
-            String data = jsonObj.get("data").getAsString();
+            String data = jsonObj.get("data").toString();
             GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
             processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
                     new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));
@@ -191,7 +195,8 @@ public class GatewaySessionCtx {
 
     private String checkDeviceConnected(String deviceName) {
         if (!devices.containsKey(deviceName)) {
-            throw new RuntimeException("Device is not connected!");
+            log.debug("[{}] Missing device [{}] for the gateway session", gatewaySessionId, deviceName);
+            throw new RuntimeException("Device " + deviceName + " is not connected!");
         } else {
             return deviceName;
         }

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

diff --git a/ui/package.json b/ui/package.json
index 5f00611..ddcc96e 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -33,6 +33,7 @@
     "angular-messages": "1.5.8",
     "angular-route": "1.5.8",
     "angular-sanitize": "1.5.8",
+    "angular-socialshare": "^2.3.8",
     "angular-storage": "0.0.15",
     "angular-touch": "1.5.8",
     "angular-translate": "2.13.1",
diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js
new file mode 100644
index 0000000..f685b1e
--- /dev/null
+++ b/ui/src/app/api/asset.service.js
@@ -0,0 +1,261 @@
+/*
+ * 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.asset', [])
+    .factory('assetService', AssetService)
+    .name;
+
+/*@ngInject*/
+function AssetService($http, $q, customerService, userService) {
+
+    var service = {
+        getAsset: getAsset,
+        getAssets: getAssets,
+        saveAsset: saveAsset,
+        deleteAsset: deleteAsset,
+        assignAssetToCustomer: assignAssetToCustomer,
+        unassignAssetFromCustomer: unassignAssetFromCustomer,
+        makeAssetPublic: makeAssetPublic,
+        getTenantAssets: getTenantAssets,
+        getCustomerAssets: getCustomerAssets,
+        findByQuery: findByQuery,
+        fetchAssetsByNameFilter: fetchAssetsByNameFilter
+    }
+
+    return service;
+
+    function getAsset(assetId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/asset/' + assetId;
+        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 getAssets(assetIds, config) {
+        var deferred = $q.defer();
+        var ids = '';
+        for (var i=0;i<assetIds.length;i++) {
+            if (i>0) {
+                ids += ',';
+            }
+            ids += assetIds[i];
+        }
+        var url = '/api/assets?assetIds=' + ids;
+        $http.get(url, config).then(function success(response) {
+            var assets = response.data;
+            assets.sort(function (asset1, asset2) {
+                var id1 =  asset1.id.id;
+                var id2 =  asset2.id.id;
+                var index1 = assetIds.indexOf(id1);
+                var index2 = assetIds.indexOf(id2);
+                return index1 - index2;
+            });
+            deferred.resolve(assets);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveAsset(asset, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/asset';
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.post(url, asset, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function deleteAsset(assetId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/asset/' + assetId;
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.delete(url, config).then(function success() {
+            deferred.resolve();
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function assignAssetToCustomer(customerId, assetId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/customer/' + customerId + '/asset/' + assetId;
+        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 unassignAssetFromCustomer(assetId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/customer/asset/' + assetId;
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.delete(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function makeAssetPublic(assetId, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/customer/public/asset/' + assetId;
+        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 getTenantAssets(pageLink, applyCustomersInfo, config) {
+        var deferred = $q.defer();
+        var url = '/api/tenant/assets?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            if (applyCustomersInfo) {
+                customerService.applyAssignedCustomersInfo(response.data.data).then(
+                    function success(data) {
+                        response.data.data = data;
+                        deferred.resolve(response.data);
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            } else {
+                deferred.resolve(response.data);
+            }
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config) {
+        var deferred = $q.defer();
+        var url = '/api/customer/' + customerId + '/assets?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            if (applyCustomersInfo) {
+                customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
+                    function success(data) {
+                        response.data.data = data;
+                        deferred.resolve(response.data);
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            } else {
+                deferred.resolve(response.data);
+            }
+        }, function fail() {
+            deferred.reject();
+        });
+
+        return deferred.promise;
+    }
+
+    function findByQuery(query, ignoreErrors, config) {
+        var deferred = $q.defer();
+        var url = '/api/assets';
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.post(url, query, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function fetchAssetsByNameFilter(assetNameFilter, limit, applyCustomersInfo, config) {
+        var deferred = $q.defer();
+        var user = userService.getCurrentUser();
+        var promise;
+        var pageLink = {limit: limit, textSearch: assetNameFilter};
+        if (user.authority === 'CUSTOMER_USER') {
+            var customerId = user.customerId;
+            promise = getCustomerAssets(customerId, pageLink, applyCustomersInfo, config);
+        } else {
+            promise = getTenantAssets(pageLink, applyCustomersInfo, config);
+        }
+        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);
+            }
+        );
+        return deferred.promise;
+    }
+
+}
diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js
new file mode 100644
index 0000000..35c14fb
--- /dev/null
+++ b/ui/src/app/api/attribute.service.js
@@ -0,0 +1,234 @@
+/*
+ * 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.attribute', [])
+    .factory('attributeService', AttributeService)
+    .name;
+
+/*@ngInject*/
+function AttributeService($http, $q, $filter, types, telemetryWebsocketService) {
+
+    var entityAttributesSubscriptionMap = {};
+
+    var service = {
+        getEntityKeys: getEntityKeys,
+        getEntityTimeseriesValues: getEntityTimeseriesValues,
+        getEntityAttributes: getEntityAttributes,
+        subscribeForEntityAttributes: subscribeForEntityAttributes,
+        unsubscribeForEntityAttributes: unsubscribeForEntityAttributes,
+        saveEntityAttributes: saveEntityAttributes,
+        deleteEntityAttributes: deleteEntityAttributes
+    }
+
+    return service;
+
+    function getEntityKeys(entityType, entityId, query, type) {
+        var deferred = $q.defer();
+        var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/keys/';
+        if (type === types.dataKeyType.timeseries) {
+            url += 'timeseries';
+        } else if (type === types.dataKeyType.attribute) {
+            url += 'attributes';
+        }
+        $http.get(url, null).then(function success(response) {
+            var result = [];
+            if (response.data) {
+                if (query) {
+                    var dataKeys = response.data;
+                    var lowercaseQuery = angular.lowercase(query);
+                    for (var i=0; i<dataKeys.length;i++) {
+                        if (angular.lowercase(dataKeys[i]).indexOf(lowercaseQuery) === 0) {
+                            result.push(dataKeys[i]);
+                        }
+                    }
+                } else {
+                    result = response.data;
+                }
+            }
+            deferred.resolve(result);
+        }, function fail(response) {
+            deferred.reject(response.data);
+        });
+        return deferred.promise;
+    }
+
+    function getEntityTimeseriesValues(entityType, entityId, keys, startTs, endTs, limit) {
+        var deferred = $q.defer();
+        var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/values/timeseries';
+        url += '?keys=' + keys;
+        url += '&startTs=' + startTs;
+        url += '&endTs=' + endTs;
+        if (angular.isDefined(limit)) {
+            url += '&limit=' + limit;
+        }
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail(response) {
+            deferred.reject(response.data);
+        });
+        return deferred.promise;
+    }
+
+    function processAttributes(attributes, query, deferred, successCallback, update, apply) {
+        attributes = $filter('orderBy')(attributes, query.order);
+        if (query.search != null) {
+            attributes = $filter('filter')(attributes, {key: query.search});
+        }
+        var responseData = {
+            count: attributes.length
+        }
+        var startIndex = query.limit * (query.page - 1);
+        responseData.data = attributes.slice(startIndex, startIndex + query.limit);
+        successCallback(responseData, update, apply);
+        if (deferred) {
+            deferred.resolve();
+        }
+    }
+
+    function getEntityAttributes(entityType, entityId, attributeScope, query, successCallback, config) {
+        var deferred = $q.defer();
+        var subscriptionId = entityType + entityId + attributeScope;
+        var eas = entityAttributesSubscriptionMap[subscriptionId];
+        if (eas) {
+            if (eas.attributes) {
+                processAttributes(eas.attributes, query, deferred, successCallback);
+                eas.subscriptionCallback = function(attributes) {
+                    processAttributes(attributes, query, null, successCallback, true, true);
+                }
+            } else {
+                eas.subscriptionCallback = function(attributes) {
+                    processAttributes(attributes, query, deferred, successCallback, false, true);
+                    eas.subscriptionCallback = function(attributes) {
+                        processAttributes(attributes, query, null, successCallback, true, true);
+                    }
+                }
+            }
+        } else {
+            var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/values/attributes/' + attributeScope;
+            $http.get(url, config).then(function success(response) {
+                processAttributes(response.data, query, deferred, successCallback);
+            }, function fail() {
+                deferred.reject();
+            });
+        }
+        return deferred;
+    }
+
+    function onSubscriptionData(data, subscriptionId) {
+        var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId];
+        if (entityAttributesSubscription) {
+            if (!entityAttributesSubscription.attributes) {
+                entityAttributesSubscription.attributes = [];
+                entityAttributesSubscription.keys = {};
+            }
+            var attributes = entityAttributesSubscription.attributes;
+            var keys = entityAttributesSubscription.keys;
+            for (var key in data) {
+                var index = keys[key];
+                var attribute;
+                if (index > -1) {
+                    attribute = attributes[index];
+                } else {
+                    attribute = {
+                        key: key
+                    };
+                    index = attributes.push(attribute)-1;
+                    keys[key] = index;
+                }
+                var attrData = data[key][0];
+                attribute.lastUpdateTs = attrData[0];
+                attribute.value = attrData[1];
+            }
+            if (entityAttributesSubscription.subscriptionCallback) {
+                entityAttributesSubscription.subscriptionCallback(attributes);
+            }
+        }
+    }
+
+    function subscribeForEntityAttributes(entityType, entityId, attributeScope) {
+        var subscriptionId = entityType + entityId + attributeScope;
+        var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId];
+        if (!entityAttributesSubscription) {
+            var subscriptionCommand = {
+                entityType: entityType,
+                entityId: entityId,
+                scope: attributeScope
+            };
+
+            var type = attributeScope === types.latestTelemetry.value ?
+                types.dataKeyType.timeseries : types.dataKeyType.attribute;
+
+            var subscriber = {
+                subscriptionCommand: subscriptionCommand,
+                type: type,
+                onData: function (data) {
+                    if (data.data) {
+                        onSubscriptionData(data.data, subscriptionId);
+                    }
+                }
+            };
+            entityAttributesSubscription = {
+                subscriber: subscriber,
+                attributes: null
+            };
+            entityAttributesSubscriptionMap[subscriptionId] = entityAttributesSubscription;
+            telemetryWebsocketService.subscribe(subscriber);
+        }
+        return subscriptionId;
+    }
+
+    function unsubscribeForEntityAttributes(subscriptionId) {
+        var entityAttributesSubscription = entityAttributesSubscriptionMap[subscriptionId];
+        if (entityAttributesSubscription) {
+            telemetryWebsocketService.unsubscribe(entityAttributesSubscription.subscriber);
+            delete entityAttributesSubscriptionMap[subscriptionId];
+        }
+    }
+
+    function saveEntityAttributes(entityType, entityId, attributeScope, attributes) {
+        var deferred = $q.defer();
+        var attributesData = {};
+        for (var a=0; a<attributes.length;a++) {
+            attributesData[attributes[a].key] = attributes[a].value;
+        }
+        var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope;
+        $http.post(url, attributesData).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail(response) {
+            deferred.reject(response.data);
+        });
+        return deferred.promise;
+    }
+
+    function deleteEntityAttributes(entityType, entityId, attributeScope, attributes) {
+        var deferred = $q.defer();
+        var keys = '';
+        for (var i = 0; i < attributes.length; i++) {
+            if (i > 0) {
+                keys += ',';
+            }
+            keys += attributes[i].key;
+        }
+        var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys;
+        $http.delete(url).then(function success() {
+            deferred.resolve();
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+
+}
\ No newline at end of file
diff --git a/ui/src/app/api/customer.service.js b/ui/src/app/api/customer.service.js
index 133021b..8a3fe66 100644
--- a/ui/src/app/api/customer.service.js
+++ b/ui/src/app/api/customer.service.js
@@ -18,12 +18,14 @@ export default angular.module('thingsboard.api.customer', [])
     .name;
 
 /*@ngInject*/
-function CustomerService($http, $q) {
+function CustomerService($http, $q, types) {
 
     var service = {
         getCustomers: getCustomers,
         getCustomer: getCustomer,
-        getCustomerTitle: getCustomerTitle,
+        getShortCustomerInfo: getShortCustomerInfo,
+        applyAssignedCustomersInfo: applyAssignedCustomersInfo,
+        applyAssignedCustomerInfo: applyAssignedCustomerInfo,
         deleteCustomer: deleteCustomer,
         saveCustomer: saveCustomer
     }
@@ -72,6 +74,88 @@ function CustomerService($http, $q) {
         return deferred.promise;
     }
 
+    function getShortCustomerInfo(customerId) {
+        var deferred = $q.defer();
+        var url = '/api/customer/' + customerId + '/shortInfo';
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail(response) {
+            deferred.reject(response.data);
+        });
+        return deferred.promise;
+    }
+
+    function applyAssignedCustomersInfo(items) {
+        var deferred = $q.defer();
+        var assignedCustomersMap = {};
+        function loadNextCustomerInfoOrComplete(i) {
+            i++;
+            if (i < items.length) {
+                loadNextCustomerInfo(i);
+            } else {
+                deferred.resolve(items);
+            }
+        }
+
+        function loadNextCustomerInfo(i) {
+            var item = items[i];
+            item.assignedCustomer = {};
+            if (item.customerId && item.customerId.id != types.id.nullUid) {
+                item.assignedCustomer.id = item.customerId.id;
+                var assignedCustomer = assignedCustomersMap[item.customerId.id];
+                if (assignedCustomer){
+                    item.assignedCustomer = assignedCustomer;
+                    loadNextCustomerInfoOrComplete(i);
+                } else {
+                    getShortCustomerInfo(item.customerId.id).then(
+                        function success(info) {
+                            assignedCustomer = {
+                                id: item.customerId.id,
+                                title: info.title,
+                                isPublic: info.isPublic
+                            };
+                            assignedCustomersMap[assignedCustomer.id] = assignedCustomer;
+                            item.assignedCustomer = assignedCustomer;
+                            loadNextCustomerInfoOrComplete(i);
+                        },
+                        function fail() {
+                            loadNextCustomerInfoOrComplete(i);
+                        }
+                    );
+                }
+            } else {
+                loadNextCustomerInfoOrComplete(i);
+            }
+        }
+        if (items.length > 0) {
+            loadNextCustomerInfo(0);
+        } else {
+            deferred.resolve(items);
+        }
+        return deferred.promise;
+    }
+
+    function applyAssignedCustomerInfo(items, customerId) {
+        var deferred = $q.defer();
+        getShortCustomerInfo(customerId).then(
+            function success(info) {
+                var assignedCustomer = {
+                    id: customerId,
+                    title: info.title,
+                    isPublic: info.isPublic
+                }
+                items.forEach(function(item) {
+                    item.assignedCustomer = assignedCustomer;
+                });
+                deferred.resolve(items);
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
     function saveCustomer(customer) {
         var deferred = $q.defer();
         var url = '/api/customer';
diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js
index be450ed..b2a7897 100644
--- a/ui/src/app/api/dashboard.service.js
+++ b/ui/src/app/api/dashboard.service.js
@@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', [])
     .factory('dashboardService', DashboardService).name;
 
 /*@ngInject*/
-function DashboardService($http, $q) {
+function DashboardService($http, $q, $location, customerService) {
 
     var service = {
         assignDashboardToCustomer: assignDashboardToCustomer,
@@ -27,7 +27,9 @@ function DashboardService($http, $q) {
         getTenantDashboards: getTenantDashboards,
         deleteDashboard: deleteDashboard,
         saveDashboard: saveDashboard,
-        unassignDashboardFromCustomer: unassignDashboardFromCustomer
+        unassignDashboardFromCustomer: unassignDashboardFromCustomer,
+        makeDashboardPublic: makeDashboardPublic,
+        getPublicDashboardLink: getPublicDashboardLink
     }
 
     return service;
@@ -45,7 +47,15 @@ function DashboardService($http, $q) {
             url += '&textOffset=' + pageLink.textOffset;
         }
         $http.get(url, null).then(function success(response) {
-            deferred.resolve(response.data);
+            customerService.applyAssignedCustomersInfo(response.data.data).then(
+                function success(data) {
+                    response.data.data = data;
+                    deferred.resolve(response.data);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
         }, function fail() {
             deferred.reject();
         });
@@ -65,7 +75,15 @@ function DashboardService($http, $q) {
             url += '&textOffset=' + pageLink.textOffset;
         }
         $http.get(url, null).then(function success(response) {
-            deferred.resolve(response.data);
+            customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
+                function success(data) {
+                    response.data.data = data;
+                    deferred.resolve(response.data);
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
         }, function fail() {
             deferred.reject();
         });
@@ -92,8 +110,8 @@ function DashboardService($http, $q) {
         var url = '/api/dashboard/' + dashboardId;
         $http.get(url, null).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -103,8 +121,8 @@ function DashboardService($http, $q) {
         var url = '/api/dashboard';
         $http.post(url, dashboard).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -114,8 +132,8 @@ function DashboardService($http, $q) {
         var url = '/api/dashboard/' + dashboardId;
         $http.delete(url).then(function success() {
             deferred.resolve();
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -123,10 +141,10 @@ function DashboardService($http, $q) {
     function assignDashboardToCustomer(customerId, dashboardId) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId;
-        $http.post(url, null).then(function success() {
-            deferred.resolve();
-        }, function fail(response) {
-            deferred.reject(response.data);
+        $http.post(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -134,12 +152,33 @@ function DashboardService($http, $q) {
     function unassignDashboardFromCustomer(dashboardId) {
         var deferred = $q.defer();
         var url = '/api/customer/dashboard/' + dashboardId;
-        $http.delete(url).then(function success() {
-            deferred.resolve();
-        }, function fail(response) {
-            deferred.reject(response.data);
+        $http.delete(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function makeDashboardPublic(dashboardId) {
+        var deferred = $q.defer();
+        var url = '/api/customer/public/dashboard/' + dashboardId;
+        $http.post(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
 
+    function getPublicDashboardLink(dashboard) {
+        var url = $location.protocol() + '://' + $location.host();
+        var port = $location.port();
+        if (port != 80 && port != 443) {
+            url += ":" + port;
+        }
+        url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.customerId.id;
+        return url;
+    }
+
 }
diff --git a/ui/src/app/api/datasource.service.js b/ui/src/app/api/datasource.service.js
index 6041cbd..27da468 100644
--- a/ui/src/app/api/datasource.service.js
+++ b/ui/src/app/api/datasource.service.js
@@ -35,11 +35,10 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t
 
     return service;
 
-
     function subscribeToDatasource(listener) {
         var datasource = listener.datasource;
 
-        if (datasource.type === types.datasourceType.device && !listener.deviceId) {
+        if (datasource.type === types.datasourceType.entity && (!listener.entityId || !listener.entityType)) {
             return;
         }
 
@@ -58,14 +57,15 @@ function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, t
         var datasourceSubscription = {
             datasourceType: datasource.type,
             dataKeys: subscriptionDataKeys,
-            type: listener.widget.type
+            type: listener.subscriptionType
         };
 
-        if (listener.widget.type === types.widgetType.timeseries.value) {
+        if (listener.subscriptionType === types.widgetType.timeseries.value) {
             datasourceSubscription.subscriptionTimewindow = angular.copy(listener.subscriptionTimewindow);
         }
-        if (datasourceSubscription.datasourceType === types.datasourceType.device) {
-            datasourceSubscription.deviceId = listener.deviceId;
+        if (datasourceSubscription.datasourceType === types.datasourceType.entity) {
+            datasourceSubscription.entityType = listener.entityType;
+            datasourceSubscription.entityId = listener.entityId;
         }
 
         listener.datasourceSubscriptionKey = utils.objectHashCode(datasourceSubscription);
@@ -141,7 +141,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                     dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody);
                 }
             }
-            if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
+            if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) {
                 if (datasourceType === types.datasourceType.function) {
                     key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type;
                 } else {
@@ -191,7 +191,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
     function syncListener(listener) {
         var key;
         var dataKey;
-        if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
+        if (datasourceType === types.datasourceType.entity || datasourceSubscription.type === types.widgetType.timeseries.value) {
             for (key in dataKeys) {
                 var dataKeysList = dataKeys[key];
                 for (var i = 0; i < dataKeysList.length; i++) {
@@ -220,7 +220,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
         var tsKeyNames = [];
         var dataKey;
 
-        if (datasourceType === types.datasourceType.device) {
+        if (datasourceType === types.datasourceType.entity) {
 
             //send subscribe command
 
@@ -252,7 +252,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                 if (history) {
 
                     var historyCommand = {
-                        deviceId: datasourceSubscription.deviceId,
+                        entityType: datasourceSubscription.entityType,
+                        entityId: datasourceSubscription.entityId,
                         keys: tsKeys,
                         startTs: subsTw.fixedWindow.startTimeMs,
                         endTs: subsTw.fixedWindow.endTimeMs,
@@ -266,6 +267,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                         type: types.dataKeyType.timeseries,
                         onData: function (data) {
                             if (data.data) {
+                                for (var key in data.data) {
+                                    var keyData = data.data[key];
+                                    data.data[key] = $filter('orderBy')(keyData, '+this[0]');
+                                }
                                 onData(data.data, types.dataKeyType.timeseries, true);
                             }
                         },
@@ -278,7 +283,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
                 } else {
 
                     subscriptionCommand = {
-                        deviceId: datasourceSubscription.deviceId,
+                        entityType: datasourceSubscription.entityType,
+                        entityId: datasourceSubscription.entityId,
                         keys: tsKeys
                     };
 
@@ -324,7 +330,8 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
             if (attrKeys.length > 0) {
 
                 subscriptionCommand = {
-                    deviceId: datasourceSubscription.deviceId,
+                    entityType: datasourceSubscription.entityType,
+                    entityId: datasourceSubscription.entityId,
                     keys: attrKeys
                 };
 
@@ -400,7 +407,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
             $timeout.cancel(timer);
             timer = null;
         }
-        if (datasourceType === types.datasourceType.device) {
+        if (datasourceType === types.datasourceType.entity) {
             for (var cmdId in subscribers) {
                 var subscriber = subscribers[cmdId];
                 telemetryWebsocketService.unsubscribe(subscriber);
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index b369e90..6f15363 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -20,10 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
     .name;
 
 /*@ngInject*/
-function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
-
-
-    var deviceAttributesSubscriptionMap = {};
+function DeviceService($http, $q, attributeService, customerService, types) {
 
     var service = {
         assignDeviceToCustomer: assignDeviceToCustomer,
@@ -31,15 +28,12 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
         getCustomerDevices: getCustomerDevices,
         getDevice: getDevice,
         getDevices: getDevices,
-        processDeviceAliases: processDeviceAliases,
-        checkDeviceAlias: checkDeviceAlias,
         getDeviceCredentials: getDeviceCredentials,
-        getDeviceKeys: getDeviceKeys,
-        getDeviceTimeseriesValues: getDeviceTimeseriesValues,
         getTenantDevices: getTenantDevices,
         saveDevice: saveDevice,
         saveDeviceCredentials: saveDeviceCredentials,
         unassignDeviceFromCustomer: unassignDeviceFromCustomer,
+        makeDevicePublic: makeDevicePublic,
         getDeviceAttributes: getDeviceAttributes,
         subscribeForDeviceAttributes: subscribeForDeviceAttributes,
         unsubscribeForDeviceAttributes: unsubscribeForDeviceAttributes,
@@ -51,7 +45,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
 
     return service;
 
-    function getTenantDevices(pageLink, config) {
+    function getTenantDevices(pageLink, applyCustomersInfo, config) {
         var deferred = $q.defer();
         var url = '/api/tenant/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -64,14 +58,26 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
             url += '&textOffset=' + pageLink.textOffset;
         }
         $http.get(url, config).then(function success(response) {
-            deferred.resolve(response.data);
+            if (applyCustomersInfo) {
+                customerService.applyAssignedCustomersInfo(response.data.data).then(
+                    function success(data) {
+                        response.data.data = data;
+                        deferred.resolve(response.data);
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            } else {
+                deferred.resolve(response.data);
+            }
         }, function fail() {
             deferred.reject();
         });
         return deferred.promise;
     }
 
-    function getCustomerDevices(customerId, pageLink) {
+    function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
         if (angular.isDefined(pageLink.textSearch)) {
@@ -83,18 +89,35 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
         if (angular.isDefined(pageLink.textOffset)) {
             url += '&textOffset=' + pageLink.textOffset;
         }
-        $http.get(url, null).then(function success(response) {
-            deferred.resolve(response.data);
+        $http.get(url, config).then(function success(response) {
+            if (applyCustomersInfo) {
+                customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
+                    function success(data) {
+                        response.data.data = data;
+                        deferred.resolve(response.data);
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            } else {
+                deferred.resolve(response.data);
+            }
         }, function fail() {
             deferred.reject();
         });
+
         return deferred.promise;
     }
 
-    function getDevice(deviceId, ignoreErrors) {
+    function getDevice(deviceId, ignoreErrors, config) {
         var deferred = $q.defer();
         var url = '/api/device/' + deviceId;
-        $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
+        if (!config) {
+            config = {};
+        }
+        config = Object.assign(config, { ignoreErrors: ignoreErrors });
+        $http.get(url, config).then(function success(response) {
             deferred.resolve(response.data);
         }, function fail(response) {
             deferred.reject(response.data);
@@ -102,7 +125,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
         return deferred.promise;
     }
 
-    function getDevices(deviceIds) {
+    function getDevices(deviceIds, config) {
         var deferred = $q.defer();
         var ids = '';
         for (var i=0;i<deviceIds.length;i++) {
@@ -112,7 +135,7 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
             ids += deviceIds[i];
         }
         var url = '/api/devices?deviceIds=' + ids;
-        $http.get(url, null).then(function success(response) {
+        $http.get(url, config).then(function success(response) {
             var devices = response.data;
             devices.sort(function (device1, device2) {
                var id1 =  device1.id.id;
@@ -128,181 +151,13 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
         return deferred.promise;
     }
 
-    function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
-        var deferred = $q.defer();
-        var user = userService.getCurrentUser();
-        var promise;
-        var pageLink = {limit: limit, textSearch: deviceNameFilter};
-        if (user.authority === 'CUSTOMER_USER') {
-            var customerId = user.customerId;
-            promise = getCustomerDevices(customerId, pageLink);
-        } else {
-            promise = getTenantDevices(pageLink);
-        }
-        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);
-            }
-        );
-        return deferred.promise;
-    }
-
-    function deviceToDeviceInfo(device) {
-        return { name: device.name, id: device.id.id };
-    }
-
-    function devicesToDevicesInfo(devices) {
-        var devicesInfo = [];
-        for (var d = 0; d < devices.length; d++) {
-            devicesInfo.push(deviceToDeviceInfo(devices[d]));
-        }
-        return devicesInfo;
-    }
-
-    function processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred) {
-        if (index < aliasIds.length) {
-            var aliasId = aliasIds[index];
-            var deviceAlias = deviceAliases[aliasId];
-            var alias = deviceAlias.alias;
-            if (!deviceAlias.deviceFilter) {
-                getDevice(deviceAlias.deviceId).then(
-                    function success(device) {
-                        var resolvedAlias = {alias: alias, deviceId: device.id.id};
-                        resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
-                        resolution.aliasesInfo.deviceAliasesInfo[aliasId] = [
-                            deviceToDeviceInfo(device)
-                        ];
-                        index++;
-                        processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
-                    },
-                    function fail() {
-                        if (!resolution.error) {
-                            resolution.error = 'dashboard.invalid-aliases-config';
-                        }
-                        index++;
-                        processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
-                    }
-                );
-            } else {
-                var deviceFilter = deviceAlias.deviceFilter;
-                if (deviceFilter.useFilter) {
-                    var deviceNameFilter = deviceFilter.deviceNameFilter;
-                    fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
-                        function(devices) {
-                            if (devices && devices != null) {
-                                var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
-                                resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
-                                resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
-                                index++;
-                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
-                            } else {
-                                if (!resolution.error) {
-                                    resolution.error = 'dashboard.invalid-aliases-config';
-                                }
-                                index++;
-                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
-                            }
-                        });
-                } else {
-                    var deviceList = deviceFilter.deviceList;
-                    getDevices(deviceList).then(
-                        function success(devices) {
-                            if (devices && devices.length > 0) {
-                                var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
-                                resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
-                                resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
-                                index++;
-                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
-                            } else {
-                                if (!resolution.error) {
-                                    resolution.error = 'dashboard.invalid-aliases-config';
-                                }
-                                index++;
-                                processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
-                            }
-                        },
-                        function fail() {
-                            if (!resolution.error) {
-                                resolution.error = 'dashboard.invalid-aliases-config';
-                            }
-                            index++;
-                            processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
-                        }
-                    );
-                }
-            }
-        } else {
-            deferred.resolve(resolution);
-        }
-    }
-
-    function processDeviceAliases(deviceAliases) {
-        var deferred = $q.defer();
-        var resolution = {
-            aliasesInfo: {
-                deviceAliases: {},
-                deviceAliasesInfo: {}
-            }
-        };
-        var aliasIds = [];
-        if (deviceAliases) {
-            for (var aliasId in deviceAliases) {
-                aliasIds.push(aliasId);
-            }
-        }
-        processDeviceAlias(0, aliasIds, deviceAliases, resolution, deferred);
-        return deferred.promise;
-    }
-
-    function checkDeviceAlias(deviceAlias) {
-        var deferred = $q.defer();
-        var deviceFilter;
-        if (deviceAlias.deviceId) {
-            deviceFilter = {
-                useFilter: false,
-                deviceNameFilter: '',
-                deviceList: [deviceAlias.deviceId]
-            }
-        } else {
-            deviceFilter = deviceAlias.deviceFilter;
-        }
-        var promise;
-        if (deviceFilter.useFilter) {
-            var deviceNameFilter = deviceFilter.deviceNameFilter;
-            promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
-        } else {
-            var deviceList = deviceFilter.deviceList;
-            promise = getDevices(deviceList);
-        }
-        promise.then(
-            function success(devices) {
-                if (devices && devices.length > 0) {
-                    deferred.resolve(true);
-                } else {
-                    deferred.resolve(false);
-                }
-            },
-            function fail() {
-                deferred.resolve(false);
-            }
-        );
-        return deferred.promise;
-    }
-
     function saveDevice(device) {
         var deferred = $q.defer();
         var url = '/api/device';
         $http.post(url, device).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -312,8 +167,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
         var url = '/api/device/' + deviceId;
         $http.delete(url).then(function success() {
             deferred.resolve();
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -323,8 +178,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
         var url = '/api/device/' + deviceId + '/credentials';
         $http.get(url, null).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -334,8 +189,8 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
         var url = '/api/device/credentials';
         $http.post(url, deviceCredentials).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -343,10 +198,10 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
     function assignDeviceToCustomer(customerId, deviceId) {
         var deferred = $q.defer();
         var url = '/api/customer/' + customerId + '/device/' + deviceId;
-        $http.post(url, null).then(function success() {
-            deferred.resolve();
-        }, function fail(response) {
-            deferred.reject(response.data);
+        $http.post(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
@@ -354,206 +209,43 @@ function DeviceService($http, $q, $filter, userService, telemetryWebsocketServic
     function unassignDeviceFromCustomer(deviceId) {
         var deferred = $q.defer();
         var url = '/api/customer/device/' + deviceId;
-        $http.delete(url).then(function success() {
-            deferred.resolve();
-        }, function fail(response) {
-            deferred.reject(response.data);
-        });
-        return deferred.promise;
-    }
-
-    function getDeviceKeys(deviceId, query, type) {
-        var deferred = $q.defer();
-        var url = '/api/plugins/telemetry/' + deviceId + '/keys/';
-        if (type === types.dataKeyType.timeseries) {
-            url += 'timeseries';
-        } else if (type === types.dataKeyType.attribute) {
-            url += 'attributes';
-        }
-        $http.get(url, null).then(function success(response) {
-            var result = [];
-            if (response.data) {
-                if (query) {
-                    var dataKeys = response.data;
-                    var lowercaseQuery = angular.lowercase(query);
-                    for (var i=0; i<dataKeys.length;i++) {
-                        if (angular.lowercase(dataKeys[i]).indexOf(lowercaseQuery) === 0) {
-                            result.push(dataKeys[i]);
-                        }
-                    }
-                } else {
-                    result = response.data;
-                }
-            }
-            deferred.resolve(result);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        $http.delete(url).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
 
-    function getDeviceTimeseriesValues(deviceId, keys, startTs, endTs, limit) {
+    function makeDevicePublic(deviceId) {
         var deferred = $q.defer();
-        var url = '/api/plugins/telemetry/' + deviceId + '/values/timeseries';
-        url += '?keys=' + keys;
-        url += '&startTs=' + startTs;
-        url += '&endTs=' + endTs;
-        if (angular.isDefined(limit)) {
-            url += '&limit=' + limit;
-        }
-        $http.get(url, null).then(function success(response) {
+        var url = '/api/customer/public/device/' + deviceId;
+        $http.post(url, null).then(function success(response) {
             deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
+        }, function fail() {
+            deferred.reject();
         });
         return deferred.promise;
     }
 
-    function processDeviceAttributes(attributes, query, deferred, successCallback, update, apply) {
-        attributes = $filter('orderBy')(attributes, query.order);
-        if (query.search != null) {
-            attributes = $filter('filter')(attributes, {key: query.search});
-        }
-        var responseData = {
-            count: attributes.length
-        }
-        var startIndex = query.limit * (query.page - 1);
-        responseData.data = attributes.slice(startIndex, startIndex + query.limit);
-        successCallback(responseData, update, apply);
-        if (deferred) {
-            deferred.resolve();
-        }
-    }
-
     function getDeviceAttributes(deviceId, attributeScope, query, successCallback, config) {
-        var deferred = $q.defer();
-        var subscriptionId = deviceId + attributeScope;
-        var das = deviceAttributesSubscriptionMap[subscriptionId];
-        if (das) {
-            if (das.attributes) {
-                processDeviceAttributes(das.attributes, query, deferred, successCallback);
-                das.subscriptionCallback = function(attributes) {
-                    processDeviceAttributes(attributes, query, null, successCallback, true, true);
-                }
-            } else {
-                das.subscriptionCallback = function(attributes) {
-                    processDeviceAttributes(attributes, query, deferred, successCallback, false, true);
-                    das.subscriptionCallback = function(attributes) {
-                        processDeviceAttributes(attributes, query, null, successCallback, true, true);
-                    }
-                }
-            }
-        } else {
-            var url = '/api/plugins/telemetry/' + deviceId + '/values/attributes/' + attributeScope;
-            $http.get(url, config).then(function success(response) {
-                processDeviceAttributes(response.data, query, deferred, successCallback);
-            }, function fail() {
-                deferred.reject();
-            });
-        }
-        return deferred;
-    }
-
-    function onSubscriptionData(data, subscriptionId) {
-        var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
-        if (deviceAttributesSubscription) {
-            if (!deviceAttributesSubscription.attributes) {
-                deviceAttributesSubscription.attributes = [];
-                deviceAttributesSubscription.keys = {};
-            }
-            var attributes = deviceAttributesSubscription.attributes;
-            var keys = deviceAttributesSubscription.keys;
-            for (var key in data) {
-                var index = keys[key];
-                var attribute;
-                if (index > -1) {
-                    attribute = attributes[index];
-                } else {
-                    attribute = {
-                        key: key
-                    };
-                    index = attributes.push(attribute)-1;
-                    keys[key] = index;
-                }
-                var attrData = data[key][0];
-                attribute.lastUpdateTs = attrData[0];
-                attribute.value = attrData[1];
-            }
-            if (deviceAttributesSubscription.subscriptionCallback) {
-                deviceAttributesSubscription.subscriptionCallback(attributes);
-            }
-        }
+        return attributeService.getEntityAttributes(types.entityType.device, deviceId, attributeScope, query, successCallback, config);
     }
 
     function subscribeForDeviceAttributes(deviceId, attributeScope) {
-        var subscriptionId = deviceId + attributeScope;
-        var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
-        if (!deviceAttributesSubscription) {
-            var subscriptionCommand = {
-                deviceId: deviceId,
-                scope: attributeScope
-            };
-
-            var type = attributeScope === types.latestTelemetry.value ?
-                types.dataKeyType.timeseries : types.dataKeyType.attribute;
-
-            var subscriber = {
-                subscriptionCommand: subscriptionCommand,
-                type: type,
-                onData: function (data) {
-                    if (data.data) {
-                        onSubscriptionData(data.data, subscriptionId);
-                    }
-                }
-            };
-            deviceAttributesSubscription = {
-                subscriber: subscriber,
-                attributes: null
-            }
-            deviceAttributesSubscriptionMap[subscriptionId] = deviceAttributesSubscription;
-            telemetryWebsocketService.subscribe(subscriber);
-        }
-        return subscriptionId;
+        return attributeService.subscribeForEntityAttributes(types.entityType.device, deviceId, attributeScope);
     }
+
     function unsubscribeForDeviceAttributes(subscriptionId) {
-        var deviceAttributesSubscription = deviceAttributesSubscriptionMap[subscriptionId];
-        if (deviceAttributesSubscription) {
-            telemetryWebsocketService.unsubscribe(deviceAttributesSubscription.subscriber);
-            delete deviceAttributesSubscriptionMap[subscriptionId];
-        }
+        attributeService.unsubscribeForEntityAttributes(subscriptionId);
     }
 
     function saveDeviceAttributes(deviceId, attributeScope, attributes) {
-        var deferred = $q.defer();
-        var attributesData = {};
-        for (var a=0; a<attributes.length;a++) {
-            attributesData[attributes[a].key] = attributes[a].value;
-        }
-        var url = '/api/plugins/telemetry/' + deviceId + '/' + attributeScope;
-        $http.post(url, attributesData).then(function success(response) {
-            deferred.resolve(response.data);
-        }, function fail(response) {
-            deferred.reject(response.data);
-        });
-        return deferred.promise;
+        return attributeService.saveEntityAttributes(types.entityType.device, deviceId, attributeScope, attributes);
     }
 
     function deleteDeviceAttributes(deviceId, attributeScope, attributes) {
-        var deferred = $q.defer();
-        var keys = '';
-        for (var i = 0; i < attributes.length; i++) {
-            if (i > 0) {
-                keys += ',';
-            }
-            keys += attributes[i].key;
-        }
-        var url = '/api/plugins/telemetry/' + deviceId + '/' + attributeScope + '?keys=' + keys;
-        $http.delete(url).then(function success() {
-            deferred.resolve();
-        }, function fail() {
-            deferred.reject();
-        });
-        return deferred.promise;
+        return attributeService.deleteEntityAttributes(types.entityType.device, deviceId, attributeScope, attributes);
     }
 
     function sendOneWayRpcCommand(deviceId, requestBody) {
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
new file mode 100644
index 0000000..4904a6a
--- /dev/null
+++ b/ui/src/app/api/entity.service.js
@@ -0,0 +1,477 @@
+/*
+ * 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 thingsboardTypes from '../common/types.constant';
+
+export default angular.module('thingsboard.api.entity', [thingsboardTypes])
+    .factory('entityService', EntityService)
+    .name;
+
+/*@ngInject*/
+function EntityService($http, $q, userService, deviceService,
+                       assetService, tenantService, customerService,
+                       ruleService, pluginService, types, utils) {
+    var service = {
+        getEntity: getEntity,
+        getEntities: getEntities,
+        getEntitiesByNameFilter: getEntitiesByNameFilter,
+        entityName: entityName,
+        processEntityAliases: processEntityAliases,
+        getEntityKeys: getEntityKeys,
+        checkEntityAlias: checkEntityAlias,
+        createDatasoucesFromSubscriptionsInfo: createDatasoucesFromSubscriptionsInfo
+    };
+
+    return service;
+
+    function getEntityPromise(entityType, entityId, config) {
+        var promise;
+        switch (entityType) {
+            case types.entityType.device:
+                promise = deviceService.getDevice(entityId, true, config);
+                break;
+            case types.entityType.asset:
+                promise = assetService.getAsset(entityId, true, config);
+                break;
+            case types.entityType.tenant:
+                promise = tenantService.getTenant(entityId);
+                break;
+            case types.entityType.customer:
+                promise = customerService.getCustomer(entityId);
+                break;
+            case types.entityType.rule:
+                promise = ruleService.getRule(entityId);
+                break;
+            case types.entityType.plugin:
+                promise = pluginService.getPlugin(entityId);
+                break;
+        }
+        return promise;
+    }
+
+    function getEntity(entityType, entityId, config) {
+        var deferred = $q.defer();
+        var promise = getEntityPromise(entityType, entityId, config);
+        promise.then(
+            function success(result) {
+                deferred.resolve(result);
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function getEntitiesByIdsPromise(fetchEntityFunction, entityIds) {
+        var tasks = [];
+        var deferred = $q.defer();
+        for (var i=0;i<entityIds.length;i++) {
+            tasks.push(fetchEntityFunction(entityIds[i]));
+        }
+        $q.all(tasks).then(
+            function success(entities) {
+                if (entities) {
+                    entities.sort(function (entity1, entity2) {
+                        var id1 = entity1.id.id;
+                        var id2 = entity2.id.id;
+                        var index1 = entityIds.indexOf(id1);
+                        var index2 = entityIds.indexOf(id2);
+                        return index1 - index2;
+                    });
+                    deferred.resolve(entities);
+                } else {
+                    deferred.resolve([]);
+                }
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function getEntitiesPromise(entityType, entityIds, config) {
+        var promise;
+        switch (entityType) {
+            case types.entityType.device:
+                promise = deviceService.getDevices(entityIds, config);
+                break;
+            case types.entityType.asset:
+                promise = assetService.getAssets(entityIds, config);
+                break;
+            case types.entityType.tenant:
+                promise = getEntitiesByIdsPromise(tenantService.getTenant, entityIds);
+                break;
+            case types.entityType.customer:
+                promise = getEntitiesByIdsPromise(customerService.getCustomer, entityIds);
+                break;
+            case types.entityType.rule:
+                promise = getEntitiesByIdsPromise(ruleService.getRule, entityIds);
+                break;
+            case types.entityType.plugin:
+                promise = getEntitiesByIdsPromise(pluginService.getPlugin, entityIds);
+                break;
+        }
+        return promise;
+    }
+
+    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();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function getEntitiesByPageLinkPromise(entityType, pageLink, config) {
+        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);
+                } else {
+                    promise = deviceService.getTenantDevices(pageLink, false, config);
+                }
+                break;
+            case types.entityType.asset:
+                if (user.authority === 'CUSTOMER_USER') {
+                    promise = assetService.getCustomerAssets(customerId, pageLink, false, config);
+                } else {
+                    promise = assetService.getTenantAssets(pageLink, false, config);
+                }
+                break;
+            case types.entityType.tenant:
+                promise = tenantService.getTenants(pageLink);
+                break;
+            case types.entityType.customer:
+                promise = customerService.getCustomers(pageLink);
+                break;
+            case types.entityType.rule:
+                promise = ruleService.getAllRules(pageLink);
+                break;
+            case types.entityType.plugin:
+                promise = pluginService.getAllPlugins(pageLink);
+                break;
+        }
+        return promise;
+    }
+
+    function getEntitiesByNameFilter(entityType, entityNameFilter, limit, config) {
+        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 {
+                    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;
+        }
+        return name;
+    }
+
+    function entityToEntityInfo(entityType, entity) {
+        return { name: entityName(entityType, entity), entityType: entityType, id: entity.id.id };
+    }
+
+    function entitiesToEntitiesInfo(entityType, entities) {
+        var entitiesInfo = [];
+        for (var d = 0; d < entities.length; d++) {
+            entitiesInfo.push(entityToEntityInfo(entityType, entities[d]));
+        }
+        return entitiesInfo;
+    }
+
+    function processEntityAlias(index, aliasIds, entityAliases, resolution, deferred) {
+        if (index < aliasIds.length) {
+            var aliasId = aliasIds[index];
+            var entityAlias = entityAliases[aliasId];
+            var alias = entityAlias.alias;
+            var entityFilter = entityAlias.entityFilter;
+            if (entityFilter.useFilter) {
+                var entityNameFilter = entityFilter.entityNameFilter;
+                getEntitiesByNameFilter(entityAlias.entityType, entityNameFilter, 100).then(
+                    function(entities) {
+                        if (entities && entities != null) {
+                            var resolvedAlias = {alias: alias, entityType: entityAlias.entityType, entityId: entities[0].id.id};
+                            resolution.aliasesInfo.entityAliases[aliasId] = resolvedAlias;
+                            resolution.aliasesInfo.entityAliasesInfo[aliasId] = entitiesToEntitiesInfo(entityAlias.entityType, entities);
+                            index++;
+                            processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
+                        } else {
+                            if (!resolution.error) {
+                                resolution.error = 'dashboard.invalid-aliases-config';
+                            }
+                            index++;
+                            processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
+                        }
+                    });
+            } else {
+                var entityList = entityFilter.entityList;
+                getEntities(entityAlias.entityType, entityList).then(
+                    function success(entities) {
+                        if (entities && entities.length > 0) {
+                            var resolvedAlias = {alias: alias, entityType: entityAlias.entityType, entityId: entities[0].id.id};
+                            resolution.aliasesInfo.entityAliases[aliasId] = resolvedAlias;
+                            resolution.aliasesInfo.entityAliasesInfo[aliasId] = entitiesToEntitiesInfo(entityAlias.entityType, entities);
+                            index++;
+                            processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
+                        } else {
+                            if (!resolution.error) {
+                                resolution.error = 'dashboard.invalid-aliases-config';
+                            }
+                            index++;
+                            processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
+                        }
+                    },
+                    function fail() {
+                        if (!resolution.error) {
+                            resolution.error = 'dashboard.invalid-aliases-config';
+                        }
+                        index++;
+                        processEntityAlias(index, aliasIds, entityAliases, resolution, deferred);
+                    }
+                );
+            }
+        } else {
+            deferred.resolve(resolution);
+        }
+    }
+
+    function processEntityAliases(entityAliases) {
+        var deferred = $q.defer();
+        var resolution = {
+            aliasesInfo: {
+                entityAliases: {},
+                entityAliasesInfo: {}
+            }
+        };
+        var aliasIds = [];
+        if (entityAliases) {
+            for (var aliasId in entityAliases) {
+                aliasIds.push(aliasId);
+            }
+        }
+        processEntityAlias(0, aliasIds, entityAliases, resolution, deferred);
+        return deferred.promise;
+    }
+
+    function getEntityKeys(entityType, entityId, query, type) {
+        var deferred = $q.defer();
+        var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/keys/';
+        if (type === types.dataKeyType.timeseries) {
+            url += 'timeseries';
+        } else if (type === types.dataKeyType.attribute) {
+            url += 'attributes';
+        }
+        $http.get(url, null).then(function success(response) {
+            var result = [];
+            if (response.data) {
+                if (query) {
+                    var dataKeys = response.data;
+                    var lowercaseQuery = angular.lowercase(query);
+                    for (var i=0; i<dataKeys.length;i++) {
+                        if (angular.lowercase(dataKeys[i]).indexOf(lowercaseQuery) === 0) {
+                            result.push(dataKeys[i]);
+                        }
+                    }
+                } else {
+                    result = response.data;
+                }
+            }
+            deferred.resolve(result);
+        }, function fail(response) {
+            deferred.reject(response.data);
+        });
+        return deferred.promise;
+    }
+
+    function checkEntityAlias(entityAlias) {
+        var deferred = $q.defer();
+        var entityType = entityAlias.entityType;
+        var entityFilter = entityAlias.entityFilter;
+        var promise;
+        if (entityFilter.useFilter) {
+            var entityNameFilter = entityFilter.entityNameFilter;
+            promise = getEntitiesByNameFilter(entityType, entityNameFilter, 1);
+        } else {
+            var entityList = entityFilter.entityList;
+            promise = getEntities(entityType, entityList);
+        }
+        promise.then(
+            function success(entities) {
+                if (entities && entities.length > 0) {
+                    deferred.resolve(true);
+                } else {
+                    deferred.resolve(false);
+                }
+            },
+            function fail() {
+                deferred.resolve(false);
+            }
+        );
+        return deferred.promise;
+    }
+
+    function createDatasoucesFromSubscriptionsInfo(subscriptionsInfo) {
+        var deferred = $q.defer();
+        var datasources = [];
+        processSubscriptionsInfo(0, subscriptionsInfo, datasources, deferred);
+        return deferred.promise;
+    }
+
+    function processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred) {
+        if (index < subscriptionsInfo.length) {
+            var subscriptionInfo = validateSubscriptionInfo(subscriptionsInfo[index]);
+            if (subscriptionInfo.type === types.datasourceType.entity) {
+                if (subscriptionInfo.entityId) {
+                    getEntity(subscriptionInfo.entityType, subscriptionInfo.entityId, {ignoreLoading: true}).then(
+                        function success(entity) {
+                            createDatasourceFromSubscription(subscriptionInfo, datasources, entity);
+                            index++;
+                            processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+                        },
+                        function fail() {
+                            index++;
+                            processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+                        }
+                    );
+                } else if (subscriptionInfo.entityName || subscriptionInfo.entityNamePrefix
+                    || subscriptionInfo.entityIds) {
+                    var promise;
+                    if (subscriptionInfo.entityName) {
+                        promise = getEntitiesByNameFilter(subscriptionInfo.entityType, subscriptionInfo.entityName, 1, {ignoreLoading: true});
+                    } else if (subscriptionInfo.entityNamePrefix) {
+                        promise = getEntitiesByNameFilter(subscriptionInfo.entityType, subscriptionInfo.entityNamePrefix, 100, {ignoreLoading: true});
+                    } else if (subscriptionInfo.entityIds) {
+                        promise = getEntities(subscriptionInfo.entityType, subscriptionInfo.entityIds, {ignoreLoading: true});
+                    }
+                    promise.then(
+                        function success(entities) {
+                            if (entities && entities.length > 0) {
+                                for (var i = 0; i < entities.length; i++) {
+                                    var entity = entities[i];
+                                    createDatasourceFromSubscription(subscriptionInfo, datasources, entity);
+                                }
+                            }
+                            index++;
+                            processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+                        },
+                        function fail() {
+                            index++;
+                            processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+                        }
+                    )
+                } else {
+                    index++;
+                    processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+                }
+            } else if (subscriptionInfo.type === types.datasourceType.function) {
+                createDatasourceFromSubscription(subscriptionInfo, datasources);
+                index++;
+                processSubscriptionsInfo(index, subscriptionsInfo, datasources, deferred);
+            }
+        } else {
+            deferred.resolve(datasources);
+        }
+    }
+
+    function validateSubscriptionInfo(subscriptionInfo) {
+        if (subscriptionInfo.type === 'device') {
+            subscriptionInfo.type = types.datasourceType.entity;
+            subscriptionInfo.entityType = types.entityType.device;
+            if (subscriptionInfo.deviceId) {
+                subscriptionInfo.entityId = subscriptionInfo.deviceId;
+            } else if (subscriptionInfo.deviceName) {
+                subscriptionInfo.entityName = subscriptionInfo.deviceName;
+            } else if (subscriptionInfo.deviceNamePrefix) {
+                subscriptionInfo.entityNamePrefix = subscriptionInfo.deviceNamePrefix;
+            } else if (subscriptionInfo.deviceIds) {
+                subscriptionInfo.entityIds = subscriptionInfo.deviceIds;
+            }
+        }
+        return subscriptionInfo;
+    }
+
+    function createDatasourceFromSubscription(subscriptionInfo, datasources, entity) {
+        var datasource;
+        if (subscriptionInfo.type === types.datasourceType.entity) {
+            datasource = {
+                type: subscriptionInfo.type,
+                entityName: entity.name ? entity.name : entity.title,
+                name: entity.name ? entity.name : entity.title,
+                entityType: subscriptionInfo.entityType,
+                entityId: entity.id.id,
+                dataKeys: []
+            }
+        } else if (subscriptionInfo.type === types.datasourceType.function) {
+            datasource = {
+                type: subscriptionInfo.type,
+                name: subscriptionInfo.name || types.datasourceType.function,
+                dataKeys: []
+            }
+        }
+        datasources.push(datasource);
+        if (subscriptionInfo.timeseries) {
+            createDatasourceKeys(subscriptionInfo.timeseries, types.dataKeyType.timeseries, datasource, datasources);
+        }
+        if (subscriptionInfo.attributes) {
+            createDatasourceKeys(subscriptionInfo.attributes, types.dataKeyType.attribute, datasource, datasources);
+        }
+        if (subscriptionInfo.functions) {
+            createDatasourceKeys(subscriptionInfo.functions, types.dataKeyType.function, datasource, datasources);
+        }
+    }
+
+    function createDatasourceKeys(keyInfos, type, datasource, datasources) {
+        for (var i=0;i<keyInfos.length;i++) {
+            var keyInfo = keyInfos[i];
+            var dataKey = utils.createKey(keyInfo, type, datasources);
+            datasource.dataKeys.push(dataKey);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/api/entity-relation.service.js b/ui/src/app/api/entity-relation.service.js
new file mode 100644
index 0000000..998607a
--- /dev/null
+++ b/ui/src/app/api/entity-relation.service.js
@@ -0,0 +1,136 @@
+/*
+ * 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.entityRelation', [])
+    .factory('entityRelationService', EntityRelationService)
+    .name;
+
+/*@ngInject*/
+function EntityRelationService($http, $q) {
+
+    var service = {
+        saveRelation: saveRelation,
+        deleteRelation: deleteRelation,
+        deleteRelations: deleteRelations,
+        findByFrom: findByFrom,
+        findByFromAndType: findByFromAndType,
+        findByTo: findByTo,
+        findByToAndType: findByToAndType,
+        findByQuery: findByQuery
+    }
+
+    return service;
+
+    function saveRelation(relation) {
+        var deferred = $q.defer();
+        var url = '/api/relation';
+        $http.post(url, relation).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function deleteRelation(fromId, fromType, relationType, toId, toType) {
+        var deferred = $q.defer();
+        var url = '/api/relation?fromId=' + fromId;
+        url += '&fromType=' + fromType;
+        url += '&relationType=' + relationType;
+        url += '&toId=' + toId;
+        url += '&toType=' + toType;
+        $http.delete(url).then(function success() {
+            deferred.resolve();
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function deleteRelations(entityId, entityType) {
+        var deferred = $q.defer();
+        var url = '/api/relations?entityId=' + entityId;
+        url += '&entityType=' + entityType;
+        $http.delete(url).then(function success() {
+            deferred.resolve();
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+
+    function findByFrom(fromId, fromType) {
+        var deferred = $q.defer();
+        var url = '/api/relations?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;
+        url += '&fromType=' + fromType;
+        url += '&relationType=' + relationType;
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function findByTo(toId, toType) {
+        var deferred = $q.defer();
+        var url = '/api/relations?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;
+        url += '&toType=' + toType;
+        url += '&relationType=' + relationType;
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function findByQuery(query) {
+        var deferred = $q.defer();
+        var url = '/api/relations';
+        $http.post(url, query).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+}
diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js
index 5213229..272e4df 100644
--- a/ui/src/app/api/login.service.js
+++ b/ui/src/app/api/login.service.js
@@ -25,6 +25,7 @@ function LoginService($http, $q) {
         changePassword: changePassword,
         hasUser: hasUser,
         login: login,
+        publicLogin: publicLogin,
         resetPassword: resetPassword,
         sendResetPasswordLink: sendResetPasswordLink,
     }
@@ -49,6 +50,19 @@ function LoginService($http, $q) {
         return deferred.promise;
     }
 
+    function publicLogin(publicId) {
+        var deferred = $q.defer();
+        var pubilcLoginRequest = {
+            publicId: publicId
+        };
+        $http.post('/api/auth/login/public', pubilcLoginRequest).then(function success(response) {
+            deferred.resolve(response);
+        }, function fail(response) {
+            deferred.reject(response);
+        });
+        return deferred.promise;
+    }
+
     function sendResetPasswordLink(email) {
         var deferred = $q.defer();
         var url = '/api/noauth/resetPasswordByEmail?email=' + email;
diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js
new file mode 100644
index 0000000..633bcc3
--- /dev/null
+++ b/ui/src/app/api/subscription.js
@@ -0,0 +1,681 @@
+/*
+ * 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.
+ */
+
+/*
+     options = {
+         type,
+         targetDeviceAliasIds,  // RPC
+         targetDeviceIds,       // RPC
+         datasources,
+         timeWindowConfig,
+         useDashboardTimewindow,
+         legendConfig,
+         decimals,
+         units,
+         callbacks
+    }
+ */
+
+export default class Subscription {
+    constructor(subscriptionContext, options) {
+
+        this.ctx = subscriptionContext;
+        this.type = options.type;
+        this.callbacks = options.callbacks;
+        this.id = this.ctx.utils.guid();
+        this.cafs = {};
+        this.registrations = [];
+
+        if (this.type === this.ctx.types.widgetType.rpc.value) {
+            this.callbacks.rpcStateChanged = this.callbacks.rpcStateChanged || function(){};
+            this.callbacks.onRpcSuccess = this.callbacks.onRpcSuccess || function(){};
+            this.callbacks.onRpcFailed = this.callbacks.onRpcFailed || function(){};
+            this.callbacks.onRpcErrorCleared = this.callbacks.onRpcErrorCleared || function(){};
+
+            this.targetDeviceAliasIds = options.targetDeviceAliasIds;
+            this.targetDeviceIds = options.targetDeviceIds;
+
+            this.targetDeviceAliasId = null;
+            this.targetDeviceId = null;
+
+            this.rpcRejection = null;
+            this.rpcErrorText = null;
+            this.rpcEnabled = false;
+            this.executingRpcRequest = false;
+            this.executingPromises = [];
+            this.initRpc();
+        } else {
+            this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || function(){};
+            this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || function(){};
+            this.callbacks.dataLoading = this.callbacks.dataLoading || function(){};
+            this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || function(){};
+            this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || function(){};
+
+            this.datasources = this.ctx.utils.validateDatasources(options.datasources);
+            this.datasourceListeners = [];
+            this.data = [];
+            this.hiddenData = [];
+            this.originalTimewindow = null;
+            this.timeWindow = {
+                stDiff: this.ctx.stDiff
+            }
+            this.useDashboardTimewindow = options.useDashboardTimewindow;
+
+            if (this.useDashboardTimewindow) {
+                this.timeWindowConfig = angular.copy(options.dashboardTimewindow);
+            } else {
+                this.timeWindowConfig = angular.copy(options.timeWindowConfig);
+            }
+
+            this.subscriptionTimewindow = null;
+
+            this.units = options.units || '';
+            this.decimals = angular.isDefined(options.decimals) ? options.decimals : 2;
+
+            this.loadingData = false;
+
+            if (options.legendConfig) {
+                this.legendConfig = options.legendConfig;
+                this.legendData = {
+                    keys: [],
+                    data: []
+                };
+                this.displayLegend = true;
+            } else {
+                this.displayLegend = false;
+            }
+            this.caulculateLegendData = this.displayLegend &&
+                this.type === this.ctx.types.widgetType.timeseries.value &&
+                (this.legendConfig.showMin === true ||
+                this.legendConfig.showMax === true ||
+                this.legendConfig.showAvg === true ||
+                this.legendConfig.showTotal === true);
+            this.initDataSubscription();
+        }
+    }
+
+    initDataSubscription() {
+        var dataIndex = 0;
+        for (var i = 0; i < this.datasources.length; i++) {
+            var datasource = this.datasources[i];
+            for (var a = 0; a < datasource.dataKeys.length; a++) {
+                var dataKey = datasource.dataKeys[a];
+                dataKey.pattern = angular.copy(dataKey.label);
+                var datasourceData = {
+                    datasource: datasource,
+                    dataKey: dataKey,
+                    data: []
+                };
+                this.data.push(datasourceData);
+                this.hiddenData.push({data: []});
+                if (this.displayLegend) {
+                    var legendKey = {
+                        dataKey: dataKey,
+                        dataIndex: dataIndex++
+                    };
+                    this.legendData.keys.push(legendKey);
+                    var legendKeyData = {
+                        min: null,
+                        max: null,
+                        avg: null,
+                        total: null,
+                        hidden: false
+                    };
+                    this.legendData.data.push(legendKeyData);
+                }
+            }
+        }
+
+        var subscription = this;
+        var registration;
+
+        if (this.displayLegend) {
+            this.legendData.keys = this.ctx.$filter('orderBy')(this.legendData.keys, '+label');
+            registration = this.ctx.$scope.$watch(
+                function() {
+                    return subscription.legendData.data;
+                },
+                function (newValue, oldValue) {
+                    for(var i = 0; i < newValue.length; i++) {
+                        if(newValue[i].hidden != oldValue[i].hidden) {
+                            subscription.updateDataVisibility(i);
+                        }
+                    }
+                }, true);
+            this.registrations.push(registration);
+        }
+
+        if (this.type === this.ctx.types.widgetType.timeseries.value) {
+            if (this.useDashboardTimewindow) {
+                registration = this.ctx.$scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
+                    if (!angular.equals(subscription.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
+                        subscription.timeWindowConfig = angular.copy(newDashboardTimewindow);
+                        subscription.unsubscribe();
+                        subscription.subscribe();
+                    }
+                });
+                this.registrations.push(registration);
+            } else {
+                this.startWatchingTimewindow();
+            }
+        }
+    }
+
+    startWatchingTimewindow() {
+        var subscription = this;
+        this.timeWindowWatchRegistration = this.ctx.$scope.$watch(function () {
+            return subscription.timeWindowConfig;
+        }, function (newTimewindow, prevTimewindow) {
+            if (!angular.equals(newTimewindow, prevTimewindow)) {
+                subscription.unsubscribe();
+                subscription.subscribe();
+            }
+        }, true);
+        this.registrations.push(this.timeWindowWatchRegistration);
+    }
+
+    stopWatchingTimewindow() {
+        if (this.timeWindowWatchRegistration) {
+            this.timeWindowWatchRegistration();
+            var index = this.registrations.indexOf(this.timeWindowWatchRegistration);
+            if (index > -1) {
+                this.registrations.splice(index, 1);
+            }
+        }
+    }
+
+    initRpc() {
+        if (this.targetDeviceAliasIds && this.targetDeviceAliasIds.length > 0) {
+            this.targetDeviceAliasId = this.targetDeviceAliasIds[0];
+            if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) {
+                this.targetDeviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId;
+            }
+        } else if (this.targetDeviceIds && this.targetDeviceIds.length > 0) {
+            this.targetDeviceId = this.targetDeviceIds[0];
+        }
+
+        if (this.targetDeviceId) {
+            this.rpcEnabled = true;
+        } else {
+            this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
+        }
+        this.callbacks.rpcStateChanged(this);
+    }
+
+    clearRpcError() {
+        this.rpcRejection = null;
+        this.rpcErrorText = null;
+        this.callbacks.onRpcErrorCleared(this);
+    }
+
+    sendOneWayCommand(method, params, timeout) {
+        return this.sendCommand(true, method, params, timeout);
+    }
+
+    sendTwoWayCommand(method, params, timeout) {
+        return this.sendCommand(false, method, params, timeout);
+    }
+
+    sendCommand(oneWayElseTwoWay, method, params, timeout) {
+        if (!this.rpcEnabled) {
+            return this.ctx.$q.reject();
+        }
+
+        if (this.rpcRejection && this.rpcRejection.status !== 408) {
+            this.rpcRejection = null;
+            this.rpcErrorText = null;
+            this.callbacks.onRpcErrorCleared(this);
+        }
+
+        var subscription = this;
+
+        var requestBody = {
+            method: method,
+            params: params
+        };
+
+        if (timeout && timeout > 0) {
+            requestBody.timeout = timeout;
+        }
+
+        var deferred = this.ctx.$q.defer();
+        this.executingRpcRequest = true;
+        this.callbacks.rpcStateChanged(this);
+        if (this.ctx.$scope.widgetEditMode) {
+            this.ctx.$timeout(function() {
+                subscription.executingRpcRequest = false;
+                subscription.callbacks.rpcStateChanged(subscription);
+                if (oneWayElseTwoWay) {
+                    deferred.resolve();
+                } else {
+                    deferred.resolve(requestBody);
+                }
+            }, 500);
+        } else {
+            this.executingPromises.push(deferred.promise);
+            var targetSendFunction = oneWayElseTwoWay ? this.ctx.deviceService.sendOneWayRpcCommand : this.ctx.deviceService.sendTwoWayRpcCommand;
+            targetSendFunction(this.targetDeviceId, requestBody).then(
+                function success(responseBody) {
+                    subscription.rpcRejection = null;
+                    subscription.rpcErrorText = null;
+                    var index = subscription.executingPromises.indexOf(deferred.promise);
+                    if (index >= 0) {
+                        subscription.executingPromises.splice( index, 1 );
+                    }
+                    subscription.executingRpcRequest = subscription.executingPromises.length > 0;
+                    subscription.callbacks.onRpcSuccess(subscription);
+                    deferred.resolve(responseBody);
+                },
+                function fail(rejection) {
+                    var index = subscription.executingPromises.indexOf(deferred.promise);
+                    if (index >= 0) {
+                        subscription.executingPromises.splice( index, 1 );
+                    }
+                    subscription.executingRpcRequest = subscription.executingPromises.length > 0;
+                    subscription.callbacks.rpcStateChanged(subscription);
+                    if (!subscription.executingRpcRequest || rejection.status === 408) {
+                        subscription.rpcRejection = rejection;
+                        if (rejection.status === 408) {
+                            subscription.rpcErrorText = 'Device is offline.';
+                        } else {
+                            subscription.rpcErrorText =  'Error : ' + rejection.status + ' - ' + rejection.statusText;
+                            if (rejection.data && rejection.data.length > 0) {
+                                subscription.rpcErrorText += '</br>';
+                                subscription.rpcErrorText += rejection.data;
+                            }
+                        }
+                        subscription.callbacks.onRpcFailed(subscription);
+                    }
+                    deferred.reject(rejection);
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
+    updateDataVisibility(index) {
+        var hidden = this.legendData.data[index].hidden;
+        if (hidden) {
+            this.hiddenData[index].data = this.data[index].data;
+            this.data[index].data = [];
+        } else {
+            this.data[index].data = this.hiddenData[index].data;
+            this.hiddenData[index].data = [];
+        }
+        this.onDataUpdated();
+    }
+
+    onAliasesChanged() {
+        if (this.type === this.ctx.types.widgetType.rpc.value) {
+            this.checkRpcTarget();
+        } else {
+            this.checkSubscriptions();
+        }
+    }
+
+    onDataUpdated(apply) {
+        if (this.cafs['dataUpdated']) {
+            this.cafs['dataUpdated']();
+            this.cafs['dataUpdated'] = null;
+        }
+        var subscription = this;
+        this.cafs['dataUpdated'] = this.ctx.tbRaf(function() {
+            try {
+                subscription.callbacks.onDataUpdated(subscription, apply);
+            } catch (e) {
+                subscription.callbacks.onDataUpdateError(subscription, e);
+            }
+        });
+        if (apply) {
+            this.ctx.$scope.$digest();
+        }
+    }
+
+    updateTimewindowConfig(newTimewindow) {
+        this.timeWindowConfig = newTimewindow;
+    }
+
+    onResetTimewindow() {
+        if (this.useDashboardTimewindow) {
+            this.ctx.dashboardTimewindowApi.onResetTimewindow();
+        } else {
+            if (this.originalTimewindow) {
+                this.stopWatchingTimewindow();
+                this.timeWindowConfig = angular.copy(this.originalTimewindow);
+                this.originalTimewindow = null;
+                this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
+                this.unsubscribe();
+                this.subscribe();
+                this.startWatchingTimewindow();
+            }
+        }
+    }
+
+    onUpdateTimewindow(startTimeMs, endTimeMs) {
+        if (this.useDashboardTimewindow) {
+            this.ctx.dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
+        } else {
+            this.stopWatchingTimewindow();
+            if (!this.originalTimewindow) {
+                this.originalTimewindow = angular.copy(this.timeWindowConfig);
+            }
+            this.timeWindowConfig = this.ctx.timeService.toHistoryTimewindow(this.timeWindowConfig, startTimeMs, endTimeMs);
+            this.callbacks.timeWindowUpdated(this, this.timeWindowConfig);
+            this.unsubscribe();
+            this.subscribe();
+            this.startWatchingTimewindow();
+        }
+    }
+
+    notifyDataLoading() {
+        this.loadingData = true;
+        this.callbacks.dataLoading(this);
+    }
+
+    notifyDataLoaded() {
+        this.loadingData = false;
+        this.callbacks.dataLoading(this);
+    }
+
+    updateTimewindow() {
+        this.timeWindow.interval = this.subscriptionTimewindow.aggregation.interval || 1000;
+        if (this.subscriptionTimewindow.realtimeWindowMs) {
+            this.timeWindow.maxTime = (new Date).getTime() + this.timeWindow.stDiff;
+            this.timeWindow.minTime = this.timeWindow.maxTime - this.subscriptionTimewindow.realtimeWindowMs;
+        } else if (this.subscriptionTimewindow.fixedWindow) {
+            this.timeWindow.maxTime = this.subscriptionTimewindow.fixedWindow.endTimeMs;
+            this.timeWindow.minTime = this.subscriptionTimewindow.fixedWindow.startTimeMs;
+        }
+    }
+
+    updateRealtimeSubscription(subscriptionTimewindow) {
+        if (subscriptionTimewindow) {
+            this.subscriptionTimewindow = subscriptionTimewindow;
+        } else {
+            this.subscriptionTimewindow =
+                this.ctx.timeService.createSubscriptionTimewindow(
+                    this.timeWindowConfig,
+                    this.timeWindow.stDiff);
+        }
+        this.updateTimewindow();
+        return this.subscriptionTimewindow;
+    }
+
+    dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
+        this.notifyDataLoaded();
+        var update = true;
+        var currentData;
+        if (this.displayLegend && this.legendData.data[datasourceIndex + dataKeyIndex].hidden) {
+            currentData = this.hiddenData[datasourceIndex + dataKeyIndex];
+        } else {
+            currentData = this.data[datasourceIndex + dataKeyIndex];
+        }
+        if (this.type === this.ctx.types.widgetType.latest.value) {
+            var prevData = currentData.data;
+            if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {
+                var prevValue = prevData[0][1];
+                if (prevValue === sourceData.data[0][1]) {
+                    update = false;
+                }
+            }
+        }
+        if (update) {
+            if (this.subscriptionTimewindow && this.subscriptionTimewindow.realtimeWindowMs) {
+                this.updateTimewindow();
+            }
+            currentData.data = sourceData.data;
+            if (this.caulculateLegendData) {
+                this.updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
+            }
+            this.onDataUpdated(apply);
+        }
+    }
+
+    updateLegend(dataIndex, data, apply) {
+        var legendKeyData = this.legendData.data[dataIndex];
+        if (this.legendConfig.showMin) {
+            legendKeyData.min = this.ctx.widgetUtils.formatValue(calculateMin(data), this.decimals, this.units);
+        }
+        if (this.legendConfig.showMax) {
+            legendKeyData.max = this.ctx.widgetUtils.formatValue(calculateMax(data), this.decimals, this.units);
+        }
+        if (this.legendConfig.showAvg) {
+            legendKeyData.avg = this.ctx.widgetUtils.formatValue(calculateAvg(data), this.decimals, this.units);
+        }
+        if (this.legendConfig.showTotal) {
+            legendKeyData.total = this.ctx.widgetUtils.formatValue(calculateTotal(data), this.decimals, this.units);
+        }
+        this.callbacks.legendDataUpdated(this, apply !== false);
+    }
+
+    subscribe() {
+        if (this.type === this.ctx.types.widgetType.rpc.value) {
+            return;
+        }
+        this.notifyDataLoading();
+        if (this.type === this.ctx.types.widgetType.timeseries.value && this.timeWindowConfig) {
+            this.updateRealtimeSubscription();
+            if (this.subscriptionTimewindow.fixedWindow) {
+                this.onDataUpdated();
+            }
+        }
+        var index = 0;
+        for (var i = 0; i < this.datasources.length; i++) {
+            var datasource = this.datasources[i];
+            if (angular.isFunction(datasource))
+                continue;
+            var entityId = null;
+            var entityType = null;
+            if (datasource.type === this.ctx.types.datasourceType.entity) {
+                var aliasName = null;
+                var entityName = null;
+                if (datasource.entityId) {
+                    entityId = datasource.entityId;
+                    entityType = datasource.entityType;
+                    datasource.name = datasource.entityName;
+                    aliasName = datasource.entityName;
+                    entityName = datasource.entityName;
+                } else if (datasource.entityAliasId) {
+                    if (this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId]) {
+                        entityId = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityId;
+                        entityType = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].entityType;
+                        datasource.name = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias;
+                        aliasName = this.ctx.aliasesInfo.entityAliases[datasource.entityAliasId].alias;
+                        entityName = '';
+                        var entitiesInfo = this.ctx.aliasesInfo.entityAliasesInfo[datasource.entityAliasId];
+                        for (var d = 0; d < entitiesInfo.length; d++) {
+                            if (entitiesInfo[d].id === entityId) {
+                                entityName = entitiesInfo[d].name;
+                                break;
+                            }
+                        }
+                    }
+                }
+            } else {
+                datasource.name = datasource.name || this.ctx.types.datasourceType.function;
+            }
+            for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
+                updateDataKeyLabel(datasource.dataKeys[dk], datasource.name, entityName, aliasName);
+            }
+
+            var subscription = this;
+
+            var listener = {
+                subscriptionType: this.type,
+                subscriptionTimewindow: this.subscriptionTimewindow,
+                datasource: datasource,
+                entityType: entityType,
+                entityId: entityId,
+                dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
+                    subscription.dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
+                },
+                updateRealtimeSubscription: function () {
+                    this.subscriptionTimewindow = subscription.updateRealtimeSubscription();
+                    return this.subscriptionTimewindow;
+                },
+                setRealtimeSubscription: function (subscriptionTimewindow) {
+                    subscription.updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
+                },
+                datasourceIndex: index
+            };
+
+            for (var a = 0; a < datasource.dataKeys.length; a++) {
+                this.data[index + a].data = [];
+            }
+
+            index += datasource.dataKeys.length;
+
+            this.datasourceListeners.push(listener);
+            this.ctx.datasourceService.subscribeToDatasource(listener);
+        }
+    }
+
+    unsubscribe() {
+        if (this.type !== this.ctx.types.widgetType.rpc.value) {
+            for (var i = 0; i < this.datasourceListeners.length; i++) {
+                var listener = this.datasourceListeners[i];
+                this.ctx.datasourceService.unsubscribeFromDatasource(listener);
+            }
+            this.datasourceListeners = [];
+        }
+    }
+
+    checkRpcTarget() {
+        var deviceId = null;
+        if (this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId]) {
+            deviceId = this.ctx.aliasesInfo.entityAliases[this.targetDeviceAliasId].entityId;
+        }
+        if (!angular.equals(deviceId, this.targetDeviceId)) {
+            this.targetDeviceId = deviceId;
+            if (this.targetDeviceId) {
+                this.rpcEnabled = true;
+            } else {
+                this.rpcEnabled = this.ctx.$scope.widgetEditMode ? true : false;
+            }
+            this.callbacks.rpcStateChanged(this);
+        }
+    }
+
+    checkSubscriptions() {
+        var subscriptionsChanged = false;
+        for (var i = 0; i < this.datasourceListeners.length; i++) {
+            var listener = this.datasourceListeners[i];
+            var entityId = null;
+            var entityType = null;
+            var aliasName = null;
+            if (listener.datasource.type === this.ctx.types.datasourceType.entity) {
+                if (listener.datasource.entityAliasId &&
+                    this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId]) {
+                    entityId = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityId;
+                    entityType = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].entityType;
+                    aliasName = this.ctx.aliasesInfo.entityAliases[listener.datasource.entityAliasId].alias;
+                }
+                if (!angular.equals(entityId, listener.entityId) ||
+                    !angular.equals(entityType, listener.entityType) ||
+                    !angular.equals(aliasName, listener.datasource.name)) {
+                    subscriptionsChanged = true;
+                    break;
+                }
+            }
+        }
+        if (subscriptionsChanged) {
+            this.unsubscribe();
+            this.subscribe();
+        }
+    }
+
+    destroy() {
+        this.unsubscribe();
+        for (var cafId in this.cafs) {
+            if (this.cafs[cafId]) {
+                this.cafs[cafId]();
+                this.cafs[cafId] = null;
+            }
+        }
+        this.registrations.forEach(function (registration) {
+            registration();
+        });
+        this.registrations = [];
+    }
+
+}
+
+const varsRegex = /\$\{([^\}]*)\}/g;
+
+function updateDataKeyLabel(dataKey, dsName, entityName, aliasName) {
+    var pattern = dataKey.pattern;
+    var label = dataKey.pattern;
+    var match = varsRegex.exec(pattern);
+    while (match !== null) {
+        var variable = match[0];
+        var variableName = match[1];
+        if (variableName === 'dsName') {
+            label = label.split(variable).join(dsName);
+        } else if (variableName === 'entityName') {
+            label = label.split(variable).join(entityName);
+        } else if (variableName === 'deviceName') {
+            label = label.split(variable).join(entityName);
+        } else if (variableName === 'aliasName') {
+            label = label.split(variable).join(aliasName);
+        }
+        match = varsRegex.exec(pattern);
+    }
+    dataKey.label = label;
+}
+
+function calculateMin(data) {
+    if (data.length > 0) {
+        var result = Number(data[0][1]);
+        for (var i=1;i<data.length;i++) {
+            result = Math.min(result, Number(data[i][1]));
+        }
+        return result;
+    } else {
+        return null;
+    }
+}
+
+function calculateMax(data) {
+    if (data.length > 0) {
+        var result = Number(data[0][1]);
+        for (var i=1;i<data.length;i++) {
+            result = Math.max(result, Number(data[i][1]));
+        }
+        return result;
+    } else {
+        return null;
+    }
+}
+
+function calculateAvg(data) {
+    if (data.length > 0) {
+        return calculateTotal(data)/data.length;
+    } else {
+        return null;
+    }
+}
+
+function calculateTotal(data) {
+    if (data.length > 0) {
+        var result = 0;
+        for (var i = 0; i < data.length; i++) {
+            result += Number(data[i][1]);
+        }
+        return result;
+    } else {
+        return null;
+    }
+}
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index ad401df..4dc41f7 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -22,9 +22,10 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
     .name;
 
 /*@ngInject*/
-function UserService($http, $q, $rootScope, adminService, dashboardService, toast, store, jwtHelper, $translate, $state) {
+function UserService($http, $q, $rootScope, adminService, dashboardService, loginService, toast, store, jwtHelper, $translate, $state, $location) {
     var currentUser = null,
         currentUserDetails = null,
+        lastPublicDashboardId = null,
         allowedDashboardIds = [],
         userLoaded = false;
 
@@ -33,6 +34,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
     var service = {
         deleteUser: deleteUser,
         getAuthority: getAuthority,
+        isPublic: isPublic,
+        getPublicId: getPublicId,
+        parsePublicId: parsePublicId,
         isAuthenticated: isAuthenticated,
         getCurrentUser: getCurrentUser,
         getCustomerUsers: getCustomerUsers,
@@ -51,18 +55,25 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
         updateAuthorizationHeader: updateAuthorizationHeader,
         gotoDefaultPlace: gotoDefaultPlace,
         forceDefaultPlace: forceDefaultPlace,
-        logout: logout
+        updateLastPublicDashboardId: updateLastPublicDashboardId,
+        logout: logout,
+        reloadUser: reloadUser
     }
 
-    loadUser(true).then(function success() {
-        notifyUserLoaded();
-    }, function fail() {
-        notifyUserLoaded();
-    });
+    reloadUser();
 
     return service;
 
-    function updateAndValidateToken(token, prefix) {
+    function reloadUser() {
+        userLoaded = false;
+        loadUser(true).then(function success() {
+            notifyUserLoaded();
+        }, function fail() {
+            notifyUserLoaded();
+        });
+    }
+
+    function updateAndValidateToken(token, prefix, notify) {
         var valid = false;
         var tokenData = jwtHelper.decodeToken(token);
         var issuedAt = tokenData.iat;
@@ -76,7 +87,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
                 valid = true;
             }
         }
-        if (!valid) {
+        if (!valid && notify) {
             $rootScope.$broadcast('unauthenticated');
         }
     }
@@ -91,6 +102,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
     function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
         currentUser = null;
         currentUserDetails = null;
+        lastPublicDashboardId = null;
         allowedDashboardIds = [];
         if (!jwtToken) {
             clearTokenData();
@@ -98,8 +110,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
                 $rootScope.$broadcast('unauthenticated', doLogout);
             }
         } else {
-            updateAndValidateToken(jwtToken, 'jwt_token');
-            updateAndValidateToken(refreshToken, 'refresh_token');
+            updateAndValidateToken(jwtToken, 'jwt_token', true);
+            updateAndValidateToken(refreshToken, 'refresh_token', true);
             if (notify) {
                 loadUser(false).then(function success() {
                     $rootScope.$broadcast('authenticated');
@@ -213,13 +225,58 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
         }
     }
 
+    function isPublic() {
+        if (currentUser) {
+            return currentUser.isPublic;
+        } else {
+            return false;
+        }
+    }
+
+    function getPublicId() {
+        if (isPublic()) {
+            return currentUser.sub;
+        } else {
+            return null;
+        }
+    }
+
+    function parsePublicId() {
+        var token = getJwtToken();
+        if (token) {
+            var tokenData = jwtHelper.decodeToken(token);
+            if (tokenData && tokenData.isPublic) {
+                return tokenData.sub;
+            }
+        }
+        return null;
+    }
+
     function isUserLoaded() {
         return userLoaded;
     }
 
     function loadUser(doTokenRefresh) {
+
         var deferred = $q.defer();
-        if (!currentUser) {
+
+        function fetchAllowedDashboardIds() {
+            var pageLink = {limit: 100};
+            dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
+                function success(result) {
+                    var dashboards = result.data;
+                    for (var d=0;d<dashboards.length;d++) {
+                        allowedDashboardIds.push(dashboards[d].id.id);
+                    }
+                    deferred.resolve();
+                },
+                function fail() {
+                    deferred.reject();
+                }
+            );
+        }
+
+        function procceedJwtTokenValidate() {
             validateJwtToken(doTokenRefresh).then(function success() {
                 var jwtToken = store.get('jwt_token');
                 currentUser = jwtHelper.decodeToken(jwtToken);
@@ -228,29 +285,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
                 } else if (currentUser) {
                     currentUser.authority = "ANONYMOUS";
                 }
-                if (currentUser.userId) {
+                if (currentUser.isPublic) {
+                    $rootScope.forceFullscreen = true;
+                    fetchAllowedDashboardIds();
+                } else if (currentUser.userId) {
                     getUser(currentUser.userId).then(
                         function success(user) {
                             currentUserDetails = user;
                             $rootScope.forceFullscreen = false;
-                            if (currentUserDetails.additionalInfo &&
-                                currentUserDetails.additionalInfo.defaultDashboardFullscreen) {
-                                $rootScope.forceFullscreen = currentUserDetails.additionalInfo.defaultDashboardFullscreen === true;
+                            if (userForceFullscreen()) {
+                                $rootScope.forceFullscreen = true;
                             }
                             if ($rootScope.forceFullscreen && currentUser.authority === 'CUSTOMER_USER') {
-                                var pageLink = {limit: 100};
-                                dashboardService.getCustomerDashboards(currentUser.customerId, pageLink).then(
-                                    function success(result) {
-                                        var dashboards = result.data;
-                                        for (var d=0;d<dashboards.length;d++) {
-                                            allowedDashboardIds.push(dashboards[d].id.id);
-                                        }
-                                        deferred.resolve();
-                                    },
-                                    function fail() {
-                                        deferred.reject();
-                                    }
-                                );
+                                fetchAllowedDashboardIds();
                             } else {
                                 deferred.resolve();
                             }
@@ -265,6 +312,23 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
             }, function fail() {
                 deferred.reject();
             });
+        }
+
+        if (!currentUser) {
+            var locationSearch = $location.search();
+            if (locationSearch.publicId) {
+                loginService.publicLogin(locationSearch.publicId).then(function success(response) {
+                    var token = response.data.token;
+                    var refreshToken = response.data.refreshToken;
+                    updateAndValidateToken(token, 'jwt_token', false);
+                    updateAndValidateToken(refreshToken, 'refresh_token', false);
+                    procceedJwtTokenValidate();
+                }, function fail() {
+                    deferred.reject();
+                });
+            } else {
+                procceedJwtTokenValidate();
+            }
         } else {
             deferred.resolve();
         }
@@ -373,17 +437,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
     function forceDefaultPlace(to, params) {
         if (currentUser && isAuthenticated()) {
             if (currentUser.authority === 'CUSTOMER_USER') {
-                if (currentUserDetails &&
-                    currentUserDetails.additionalInfo &&
-                    currentUserDetails.additionalInfo.defaultDashboardId) {
-                    if ($rootScope.forceFullscreen) {
-                        if (to.name === 'home.profile') {
-                            return false;
-                        } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
+                if ((userHasDefaultDashboard() && $rootScope.forceFullscreen) || isPublic()) {
+                    if (to.name === 'home.profile') {
+                        if (userHasProfile()) {
                             return false;
                         } else {
                             return true;
                         }
+                    } else if (to.name === 'home.dashboards.dashboard' && allowedDashboardIds.indexOf(params.dashboardId) > -1) {
+                        return false;
+                    } else {
+                        return true;
                     }
                 }
             }
@@ -395,11 +459,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
         if (currentUser && isAuthenticated()) {
             var place = 'home.links';
             if (currentUser.authority === 'CUSTOMER_USER') {
-                if (currentUserDetails &&
-                    currentUserDetails.additionalInfo &&
-                    currentUserDetails.additionalInfo.defaultDashboardId) {
+                if (userHasDefaultDashboard()) {
                     place = 'home.dashboards.dashboard';
                     params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
+                } else if (isPublic()) {
+                    place = 'home.dashboards.dashboard';
+                    params = {dashboardId: lastPublicDashboardId};
                 }
             } else if (currentUser.authority === 'SYS_ADMIN') {
                 adminService.checkUpdates().then(
@@ -416,4 +481,27 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, toas
         }
     }
 
+    function userHasDefaultDashboard() {
+        return currentUserDetails &&
+               currentUserDetails.additionalInfo &&
+               currentUserDetails.additionalInfo.defaultDashboardId;
+    }
+
+    function userForceFullscreen() {
+        return (currentUser && currentUser.isPublic) ||
+               (currentUserDetails.additionalInfo &&
+                currentUserDetails.additionalInfo.defaultDashboardFullscreen &&
+                currentUserDetails.additionalInfo.defaultDashboardFullscreen === true);
+    }
+
+    function userHasProfile() {
+        return currentUser && !currentUser.isPublic;
+    }
+
+    function updateLastPublicDashboardId(dashboardId) {
+        if (isPublic()) {
+            lastPublicDashboardId = dashboardId;
+        }
+    }
+
 }
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index fb8565c..21c38e2 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -17,7 +17,8 @@ import $ from 'jquery';
 import moment from 'moment';
 import tinycolor from 'tinycolor2';
 
-import thinsboardLedLight from '../components/led-light.directive';
+import thingsboardLedLight from '../components/led-light.directive';
+import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
 
 import TbFlot from '../widget/lib/flot-widget';
 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
@@ -31,7 +32,8 @@ import cssjs from '../../vendor/css.js/css';
 import thingsboardTypes from '../common/types.constant';
 import thingsboardUtils from '../common/utils.service';
 
-export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thinsboardLedLight, thingsboardTypes, thingsboardUtils])
+export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
+    thingsboardTypes, thingsboardUtils])
     .factory('widgetService', WidgetService)
     .name;
 
@@ -539,6 +541,10 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
 
          '    }\n\n' +
 
+         '    self.useCustomDatasources = function() {\n\n' +
+
+         '    }\n\n' +
+
          '    self.onResize = function() {\n\n' +
 
          '    }\n\n' +
@@ -579,6 +585,11 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
             if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
                 result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
             }
+            if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) {
+                result.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
+            } else {
+                result.useCustomDatasources = false;
+            }
             return result;
         } catch (e) {
             utils.processWidgetException(e);
@@ -617,6 +628,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
                     if (widgetType.dataKeySettingsSchema) {
                         widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
                     }
+                    widgetInfo.useCustomDatasources = widgetType.useCustomDatasources;
                     putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
                     putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
                     deferred.resolve(widgetInfo);
diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js
index 9b2ad25..7f7e7a5 100644
--- a/ui/src/app/app.config.js
+++ b/ui/src/app/app.config.js
@@ -16,6 +16,9 @@
 import injectTapEventPlugin from 'react-tap-event-plugin';
 import UrlHandler from './url.handler';
 import addLocaleKorean from './locale/locale.constant-ko';
+import addLocaleChinese from './locale/locale.constant-zh';
+import addLocaleRussian from './locale/locale.constant-ru';
+import addLocaleSpanish from './locale/locale.constant-es';
 
 /* eslint-disable import/no-unresolved, import/default */
 
@@ -50,11 +53,21 @@ export default function AppConfig($provide,
     $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
 
     addLocaleKorean(locales);
+    addLocaleChinese(locales);
+    addLocaleRussian(locales);
+    addLocaleSpanish(locales);
+
     var $window = angular.injector(['ng']).get('$window');
     var lang = $window.navigator.language || $window.navigator.userLanguage;
     if (lang === 'ko') {
         $translateProvider.useSanitizeValueStrategy(null);
         $translateProvider.preferredLanguage('ko_KR');
+    } else if (lang === 'zh') {
+        $translateProvider.useSanitizeValueStrategy(null);
+        $translateProvider.preferredLanguage('zh_CN');
+    } else if (lang === 'es') {
+        $translateProvider.useSanitizeValueStrategy(null);
+        $translateProvider.preferredLanguage('es_ES');
     }
 
     for (var langKey in locales) {

ui/src/app/app.js 12(+12 -0)

diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index e3b36e3..f67be33 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -19,6 +19,7 @@ import angular from 'angular';
 import ngMaterial from 'angular-material';
 import ngMdIcons from 'angular-material-icons';
 import ngCookies from 'angular-cookies';
+import angularSocialshare from 'angular-socialshare';
 import 'angular-translate';
 import 'angular-translate-loader-static-files';
 import 'angular-translate-storage-local';
@@ -52,6 +53,7 @@ import thingsboardDialogs from './components/datakey-config-dialog.controller';
 import thingsboardMenu from './services/menu.service';
 import thingsboardRaf from './common/raf.provider';
 import thingsboardUtils from './common/utils.service';
+import thingsboardDashboardUtils from './common/dashboard-utils.service';
 import thingsboardTypes from './common/types.constant';
 import thingsboardApiTime from './api/time.service';
 import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
@@ -61,6 +63,10 @@ import thingsboardHome from './layout';
 import thingsboardApiLogin from './api/login.service';
 import thingsboardApiDevice from './api/device.service';
 import thingsboardApiUser from './api/user.service';
+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 'typeface-roboto';
 import 'font-awesome/css/font-awesome.min.css';
@@ -82,6 +88,7 @@ angular.module('thingsboard', [
     ngMaterial,
     ngMdIcons,
     ngCookies,
+    angularSocialshare,
     'pascalprecht.translate',
     'mdColorPicker',
     mdPickers,
@@ -103,6 +110,7 @@ angular.module('thingsboard', [
     thingsboardMenu,
     thingsboardRaf,
     thingsboardUtils,
+    thingsboardDashboardUtils,
     thingsboardTypes,
     thingsboardApiTime,
     thingsboardKeyboardShortcut,
@@ -112,6 +120,10 @@ angular.module('thingsboard', [
     thingsboardApiLogin,
     thingsboardApiDevice,
     thingsboardApiUser,
+    thingsboardApiEntityRelation,
+    thingsboardApiAsset,
+    thingsboardApiAttribute,
+    thingsboardApiEntity,
     uiRouter])
     .config(AppConfig)
     .factory('globalInterceptor', GlobalInterceptor)

ui/src/app/app.run.js 52(+42 -10)

diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js
index 02fe8fd..1e36600 100644
--- a/ui/src/app/app.run.js
+++ b/ui/src/app/app.run.js
@@ -36,7 +36,7 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, 
     }
 
     initWatchers();
-
+    
     function initWatchers() {
         $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
             if (doLogout) {
@@ -55,8 +55,39 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, 
         });
 
         $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
+
+            function waitForUserLoaded() {
+                if ($rootScope.userLoadedHandle) {
+                    $rootScope.userLoadedHandle();
+                }
+                $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
+                    $rootScope.userLoadedHandle();
+                    $state.go(to.name, params);
+                });
+            }
+
+            function reloadUserFromPublicId() {
+                userService.setUserFromJwtToken(null, null, false);
+                waitForUserLoaded();
+                userService.reloadUser();
+            }
+
+            var locationSearch = $location.search();
+            var publicId = locationSearch.publicId;
+
             if (userService.isUserLoaded() === true) {
                 if (userService.isAuthenticated()) {
+                    if (userService.isPublic()) {
+                        if (userService.parsePublicId() !== publicId) {
+                            evt.preventDefault();
+                            if (publicId && publicId.length > 0) {
+                                reloadUserFromPublicId();
+                            } else {
+                                userService.logout();
+                            }
+                            return;
+                        }
+                    }
                     if (userService.forceDefaultPlace(to, params)) {
                         evt.preventDefault();
                         gotoDefaultPlace(params);
@@ -75,7 +106,10 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, 
                         }
                     }
                 } else {
-                    if (to.module === 'private') {
+                    if (publicId && publicId.length > 0) {
+                        evt.preventDefault();
+                        reloadUserFromPublicId();
+                    } else if (to.module === 'private') {
                         evt.preventDefault();
                         if (to.url === '/home' || to.url === '/') {
                             $state.go('login', params);
@@ -86,19 +120,17 @@ export default function AppRun($rootScope, $window, $injector, $location, $log, 
                 }
             } else {
                 evt.preventDefault();
-                if ($rootScope.userLoadedHandle) {
-                    $rootScope.userLoadedHandle();
-                }
-                $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
-                    $rootScope.userLoadedHandle();
-                    $state.go(to.name, params);
-                });
+                waitForUserLoaded();
             }
         })
 
         $rootScope.pageTitle = 'Thingsboard';
 
-        $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to) {
+        $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) {
+            if (userService.isPublic() && to.name === 'home.dashboards.dashboard') {
+                $location.search('publicId', userService.getPublicId());
+                userService.updateLastPublicDashboardId(params.dashboardId);
+            }
             if (angular.isDefined(to.data.pageTitle)) {
                 $translate(to.data.pageTitle).then(function (translation) {
                     $rootScope.pageTitle = 'Thingsboard | ' + translation;
diff --git a/ui/src/app/asset/add-asset.tpl.html b/ui/src/app/asset/add-asset.tpl.html
new file mode 100644
index 0000000..ce22e4e
--- /dev/null
+++ b/ui/src/app/asset/add-asset.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-dialog aria-label="{{ 'asset.add' | translate }}" tb-help="'assets'" help-container-id="help-container">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>asset.add</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-asset asset="vm.item" is-edit="true" the-form="theForm"></tb-asset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/asset/add-assets-to-customer.controller.js b/ui/src/app/asset/add-assets-to-customer.controller.js
new file mode 100644
index 0000000..53feb7d
--- /dev/null
+++ b/ui/src/app/asset/add-assets-to-customer.controller.js
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+/*@ngInject*/
+export default function AddAssetsToCustomerController(assetService, $mdDialog, $q, customerId, assets) {
+
+    var vm = this;
+
+    vm.assets = assets;
+    vm.searchText = '';
+
+    vm.assign = assign;
+    vm.cancel = cancel;
+    vm.hasData = hasData;
+    vm.noData = noData;
+    vm.searchAssetTextUpdated = searchAssetTextUpdated;
+    vm.toggleAssetSelection = toggleAssetSelection;
+
+    vm.theAssets = {
+        getItemAtIndex: function (index) {
+            if (index > vm.assets.data.length) {
+                vm.theAssets.fetchMoreItems_(index);
+                return null;
+            }
+            var item = vm.assets.data[index];
+            if (item) {
+                item.indexNumber = index + 1;
+            }
+            return item;
+        },
+
+        getLength: function () {
+            if (vm.assets.hasNext) {
+                return vm.assets.data.length + vm.assets.nextPageLink.limit;
+            } else {
+                return vm.assets.data.length;
+            }
+        },
+
+        fetchMoreItems_: function () {
+            if (vm.assets.hasNext && !vm.assets.pending) {
+                vm.assets.pending = true;
+                assetService.getTenantAssets(vm.assets.nextPageLink, false).then(
+                    function success(assets) {
+                        vm.assets.data = vm.assets.data.concat(assets.data);
+                        vm.assets.nextPageLink = assets.nextPageLink;
+                        vm.assets.hasNext = assets.hasNext;
+                        if (vm.assets.hasNext) {
+                            vm.assets.nextPageLink.limit = vm.assets.pageSize;
+                        }
+                        vm.assets.pending = false;
+                    },
+                    function fail() {
+                        vm.assets.hasNext = false;
+                        vm.assets.pending = false;
+                    });
+            }
+        }
+    };
+
+    function cancel () {
+        $mdDialog.cancel();
+    }
+
+    function assign() {
+        var tasks = [];
+        for (var assetId in vm.assets.selections) {
+            tasks.push(assetService.assignAssetToCustomer(customerId, assetId));
+        }
+        $q.all(tasks).then(function () {
+            $mdDialog.hide();
+        });
+    }
+
+    function noData() {
+        return vm.assets.data.length == 0 && !vm.assets.hasNext;
+    }
+
+    function hasData() {
+        return vm.assets.data.length > 0;
+    }
+
+    function toggleAssetSelection($event, asset) {
+        $event.stopPropagation();
+        var selected = angular.isDefined(asset.selected) && asset.selected;
+        asset.selected = !selected;
+        if (asset.selected) {
+            vm.assets.selections[asset.id.id] = true;
+            vm.assets.selectedCount++;
+        } else {
+            delete vm.assets.selections[asset.id.id];
+            vm.assets.selectedCount--;
+        }
+    }
+
+    function searchAssetTextUpdated() {
+        vm.assets = {
+            pageSize: vm.assets.pageSize,
+            data: [],
+            nextPageLink: {
+                limit: vm.assets.pageSize,
+                textSearch: vm.searchText
+            },
+            selections: {},
+            selectedCount: 0,
+            hasNext: true,
+            pending: false
+        };
+    }
+
+}
diff --git a/ui/src/app/asset/add-assets-to-customer.tpl.html b/ui/src/app/asset/add-assets-to-customer.tpl.html
new file mode 100644
index 0000000..18e23ce
--- /dev/null
+++ b/ui/src/app/asset/add-assets-to-customer.tpl.html
@@ -0,0 +1,77 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'asset.assign-to-customer' | translate }}">
+    <form name="theForm" ng-submit="vm.assign()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>asset.assign-asset-to-customer</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <fieldset>
+                    <span translate>asset.assign-asset-to-customer-text</span>
+                    <md-input-container class="md-block" style='margin-bottom: 0px;'>
+                        <label>&nbsp;</label>
+                        <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+                            search
+                        </md-icon>
+                        <input id="asset-search" autofocus ng-model="vm.searchText"
+                               ng-change="vm.searchAssetTextUpdated()"
+                               placeholder="{{ 'common.enter-search' | translate }}"/>
+                    </md-input-container>
+                    <div style='min-height: 150px;'>
+					<span translate layout-align="center center"
+                          style="text-transform: uppercase; display: flex; height: 150px;"
+                          class="md-subhead"
+                          ng-show="vm.noData()">asset.no-assets-text</span>
+                        <md-virtual-repeat-container ng-show="vm.hasData()"
+                                                     tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+                                                     style='min-height: 150px; width: 100%;'>
+                            <md-list>
+                                <md-list-item md-virtual-repeat="asset in vm.theAssets" md-on-demand
+                                              class="repeated-item" flex>
+                                    <md-checkbox ng-click="vm.toggleAssetSelection($event, asset)"
+                                                 aria-label="{{ 'item.selected' | translate }}"
+                                                 ng-checked="asset.selected"></md-checkbox>
+                                    <span> {{ asset.name }} </span>
+                                </md-list-item>
+                            </md-list>
+                        </md-virtual-repeat-container>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || vm.assets.selectedCount == 0" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.assign' | translate }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js
new file mode 100644
index 0000000..d0944ae
--- /dev/null
+++ b/ui/src/app/asset/asset.controller.js
@@ -0,0 +1,506 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addAssetTemplate from './add-asset.tpl.html';
+import assetCard from './asset-card.tpl.html';
+import assignToCustomerTemplate from './assign-to-customer.tpl.html';
+import addAssetsToCustomerTemplate from './add-assets-to-customer.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export function AssetCardController(types) {
+
+    var vm = this;
+
+    vm.types = types;
+
+    vm.isAssignedToCustomer = function() {
+        if (vm.item && vm.item.customerId && vm.parentCtl.assetsScope === 'tenant' &&
+            vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
+            return true;
+        }
+        return false;
+    }
+
+    vm.isPublic = function() {
+        if (vm.item && vm.item.assignedCustomer && vm.parentCtl.assetsScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
+            return true;
+        }
+        return false;
+    }
+}
+
+
+/*@ngInject*/
+export function AssetController(userService, assetService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
+
+    var customerId = $stateParams.customerId;
+
+    var assetActionsList = [];
+
+    var assetGroupActionsList = [];
+
+    var vm = this;
+
+    vm.types = types;
+
+    vm.assetGridConfig = {
+        deleteItemTitleFunc: deleteAssetTitle,
+        deleteItemContentFunc: deleteAssetText,
+        deleteItemsTitleFunc: deleteAssetsTitle,
+        deleteItemsActionTitleFunc: deleteAssetsActionTitle,
+        deleteItemsContentFunc: deleteAssetsText,
+
+        saveItemFunc: saveAsset,
+
+        getItemTitleFunc: getAssetTitle,
+
+        itemCardController: 'AssetCardController',
+        itemCardTemplateUrl: assetCard,
+        parentCtl: vm,
+
+        actionsList: assetActionsList,
+        groupActionsList: assetGroupActionsList,
+
+        onGridInited: gridInited,
+
+        addItemTemplateUrl: addAssetTemplate,
+
+        addItemText: function() { return $translate.instant('asset.add-asset-text') },
+        noItemsText: function() { return $translate.instant('asset.no-assets-text') },
+        itemDetailsText: function() { return $translate.instant('asset.asset-details') },
+        isDetailsReadOnly: isCustomerUser,
+        isSelectionEnabled: function () {
+            return !isCustomerUser();
+        }
+    };
+
+    if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+        vm.assetGridConfig.items = $stateParams.items;
+    }
+
+    if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+        vm.assetGridConfig.topIndex = $stateParams.topIndex;
+    }
+
+    vm.assetsScope = $state.$current.data.assetsType;
+
+    vm.assignToCustomer = assignToCustomer;
+    vm.makePublic = makePublic;
+    vm.unassignFromCustomer = unassignFromCustomer;
+
+    initController();
+
+    function initController() {
+        var fetchAssetsFunction = null;
+        var deleteAssetFunction = null;
+        var refreshAssetsParamsFunction = null;
+
+        var user = userService.getCurrentUser();
+
+        if (user.authority === 'CUSTOMER_USER') {
+            vm.assetsScope = 'customer_user';
+            customerId = user.customerId;
+        }
+        if (customerId) {
+            vm.customerAssetsTitle = $translate.instant('customer.assets');
+            customerService.getShortCustomerInfo(customerId).then(
+                function success(info) {
+                    if (info.isPublic) {
+                        vm.customerAssetsTitle = $translate.instant('customer.public-assets');
+                    }
+                }
+            );
+        }
+
+        if (vm.assetsScope === 'tenant') {
+            fetchAssetsFunction = function (pageLink) {
+                return assetService.getTenantAssets(pageLink, true);
+            };
+            deleteAssetFunction = function (assetId) {
+                return assetService.deleteAsset(assetId);
+            };
+            refreshAssetsParamsFunction = function() {
+                return {"topIndex": vm.topIndex};
+            };
+
+            assetActionsList.push({
+                onAction: function ($event, item) {
+                    makePublic($event, item);
+                },
+                name: function() { return $translate.instant('action.share') },
+                details: function() { return $translate.instant('asset.make-public') },
+                icon: "share",
+                isEnabled: function(asset) {
+                    return asset && (!asset.customerId || asset.customerId.id === types.id.nullUid);
+                }
+            });
+
+            assetActionsList.push(
+                {
+                    onAction: function ($event, item) {
+                        assignToCustomer($event, [ item.id.id ]);
+                    },
+                    name: function() { return $translate.instant('action.assign') },
+                    details: function() { return $translate.instant('asset.assign-to-customer') },
+                    icon: "assignment_ind",
+                    isEnabled: function(asset) {
+                        return asset && (!asset.customerId || asset.customerId.id === types.id.nullUid);
+                    }
+                }
+            );
+
+            assetActionsList.push(
+                {
+                    onAction: function ($event, item) {
+                        unassignFromCustomer($event, item, false);
+                    },
+                    name: function() { return $translate.instant('action.unassign') },
+                    details: function() { return $translate.instant('asset.unassign-from-customer') },
+                    icon: "assignment_return",
+                    isEnabled: function(asset) {
+                        return asset && asset.customerId && asset.customerId.id !== types.id.nullUid && !asset.assignedCustomer.isPublic;
+                    }
+                }
+            );
+
+            assetActionsList.push({
+                onAction: function ($event, item) {
+                    unassignFromCustomer($event, item, true);
+                },
+                name: function() { return $translate.instant('action.make-private') },
+                details: function() { return $translate.instant('asset.make-private') },
+                icon: "reply",
+                isEnabled: function(asset) {
+                    return asset && asset.customerId && asset.customerId.id !== types.id.nullUid && asset.assignedCustomer.isPublic;
+                }
+            });
+
+            assetActionsList.push(
+                {
+                    onAction: function ($event, item) {
+                        vm.grid.deleteItem($event, item);
+                    },
+                    name: function() { return $translate.instant('action.delete') },
+                    details: function() { return $translate.instant('asset.delete') },
+                    icon: "delete"
+                }
+            );
+
+            assetGroupActionsList.push(
+                {
+                    onAction: function ($event, items) {
+                        assignAssetsToCustomer($event, items);
+                    },
+                    name: function() { return $translate.instant('asset.assign-assets') },
+                    details: function(selectedCount) {
+                        return $translate.instant('asset.assign-assets-text', {count: selectedCount}, "messageformat");
+                    },
+                    icon: "assignment_ind"
+                }
+            );
+
+            assetGroupActionsList.push(
+                {
+                    onAction: function ($event) {
+                        vm.grid.deleteItems($event);
+                    },
+                    name: function() { return $translate.instant('asset.delete-assets') },
+                    details: deleteAssetsActionTitle,
+                    icon: "delete"
+                }
+            );
+
+
+
+        } else if (vm.assetsScope === 'customer' || vm.assetsScope === 'customer_user') {
+            fetchAssetsFunction = function (pageLink) {
+                return assetService.getCustomerAssets(customerId, pageLink, true);
+            };
+            deleteAssetFunction = function (assetId) {
+                return assetService.unassignAssetFromCustomer(assetId);
+            };
+            refreshAssetsParamsFunction = function () {
+                return {"customerId": customerId, "topIndex": vm.topIndex};
+            };
+
+            if (vm.assetsScope === 'customer') {
+                assetActionsList.push(
+                    {
+                        onAction: function ($event, item) {
+                            unassignFromCustomer($event, item, false);
+                        },
+                        name: function() { return $translate.instant('action.unassign') },
+                        details: function() { return $translate.instant('asset.unassign-from-customer') },
+                        icon: "assignment_return",
+                        isEnabled: function(asset) {
+                            return asset && !asset.assignedCustomer.isPublic;
+                        }
+                    }
+                );
+                assetActionsList.push(
+                    {
+                        onAction: function ($event, item) {
+                            unassignFromCustomer($event, item, true);
+                        },
+                        name: function() { return $translate.instant('action.make-private') },
+                        details: function() { return $translate.instant('asset.make-private') },
+                        icon: "reply",
+                        isEnabled: function(asset) {
+                            return asset && asset.assignedCustomer.isPublic;
+                        }
+                    }
+                );
+
+                assetGroupActionsList.push(
+                    {
+                        onAction: function ($event, items) {
+                            unassignAssetsFromCustomer($event, items);
+                        },
+                        name: function() { return $translate.instant('asset.unassign-assets') },
+                        details: function(selectedCount) {
+                            return $translate.instant('asset.unassign-assets-action-title', {count: selectedCount}, "messageformat");
+                        },
+                        icon: "assignment_return"
+                    }
+                );
+
+                vm.assetGridConfig.addItemAction = {
+                    onAction: function ($event) {
+                        addAssetsToCustomer($event);
+                    },
+                    name: function() { return $translate.instant('asset.assign-assets') },
+                    details: function() { return $translate.instant('asset.assign-new-asset') },
+                    icon: "add"
+                };
+
+
+            } else if (vm.assetsScope === 'customer_user') {
+                vm.assetGridConfig.addItemAction = {};
+            }
+        }
+
+        vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction;
+        vm.assetGridConfig.fetchItemsFunc = fetchAssetsFunction;
+        vm.assetGridConfig.deleteItemFunc = deleteAssetFunction;
+
+    }
+
+    function deleteAssetTitle(asset) {
+        return $translate.instant('asset.delete-asset-title', {assetName: asset.name});
+    }
+
+    function deleteAssetText() {
+        return $translate.instant('asset.delete-asset-text');
+    }
+
+    function deleteAssetsTitle(selectedCount) {
+        return $translate.instant('asset.delete-assets-title', {count: selectedCount}, 'messageformat');
+    }
+
+    function deleteAssetsActionTitle(selectedCount) {
+        return $translate.instant('asset.delete-assets-action-title', {count: selectedCount}, 'messageformat');
+    }
+
+    function deleteAssetsText () {
+        return $translate.instant('asset.delete-assets-text');
+    }
+
+    function gridInited(grid) {
+        vm.grid = grid;
+    }
+
+    function getAssetTitle(asset) {
+        return asset ? asset.name : '';
+    }
+
+    function saveAsset(asset) {
+        var deferred = $q.defer();
+        assetService.saveAsset(asset).then(
+            function success(savedAsset) {
+                var assets = [ savedAsset ];
+                customerService.applyAssignedCustomersInfo(assets).then(
+                    function success(items) {
+                        if (items && items.length == 1) {
+                            deferred.resolve(items[0]);
+                        } else {
+                            deferred.reject();
+                        }
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function isCustomerUser() {
+        return vm.assetsScope === 'customer_user';
+    }
+
+    function assignToCustomer($event, assetIds) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var pageSize = 10;
+        customerService.getCustomers({limit: pageSize, textSearch: ''}).then(
+            function success(_customers) {
+                var customers = {
+                    pageSize: pageSize,
+                    data: _customers.data,
+                    nextPageLink: _customers.nextPageLink,
+                    selection: null,
+                    hasNext: _customers.hasNext,
+                    pending: false
+                };
+                if (customers.hasNext) {
+                    customers.nextPageLink.limit = pageSize;
+                }
+                $mdDialog.show({
+                    controller: 'AssignAssetToCustomerController',
+                    controllerAs: 'vm',
+                    templateUrl: assignToCustomerTemplate,
+                    locals: {assetIds: assetIds, customers: customers},
+                    parent: angular.element($document[0].body),
+                    fullscreen: true,
+                    targetEvent: $event
+                }).then(function () {
+                    vm.grid.refreshList();
+                }, function () {
+                });
+            },
+            function fail() {
+            });
+    }
+
+    function addAssetsToCustomer($event) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var pageSize = 10;
+        assetService.getTenantAssets({limit: pageSize, textSearch: ''}, false).then(
+            function success(_assets) {
+                var assets = {
+                    pageSize: pageSize,
+                    data: _assets.data,
+                    nextPageLink: _assets.nextPageLink,
+                    selections: {},
+                    selectedCount: 0,
+                    hasNext: _assets.hasNext,
+                    pending: false
+                };
+                if (assets.hasNext) {
+                    assets.nextPageLink.limit = pageSize;
+                }
+                $mdDialog.show({
+                    controller: 'AddAssetsToCustomerController',
+                    controllerAs: 'vm',
+                    templateUrl: addAssetsToCustomerTemplate,
+                    locals: {customerId: customerId, assets: assets},
+                    parent: angular.element($document[0].body),
+                    fullscreen: true,
+                    targetEvent: $event
+                }).then(function () {
+                    vm.grid.refreshList();
+                }, function () {
+                });
+            },
+            function fail() {
+            });
+    }
+
+    function assignAssetsToCustomer($event, items) {
+        var assetIds = [];
+        for (var id in items.selections) {
+            assetIds.push(id);
+        }
+        assignToCustomer($event, assetIds);
+    }
+
+    function unassignFromCustomer($event, asset, isPublic) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var title;
+        var content;
+        var label;
+        if (isPublic) {
+            title = $translate.instant('asset.make-private-asset-title', {assetName: asset.name});
+            content = $translate.instant('asset.make-private-asset-text');
+            label = $translate.instant('asset.make-private');
+        } else {
+            title = $translate.instant('asset.unassign-asset-title', {assetName: asset.name});
+            content = $translate.instant('asset.unassign-asset-text');
+            label = $translate.instant('asset.unassign-asset');
+        }
+        var confirm = $mdDialog.confirm()
+            .targetEvent($event)
+            .title(title)
+            .htmlContent(content)
+            .ariaLabel(label)
+            .cancel($translate.instant('action.no'))
+            .ok($translate.instant('action.yes'));
+        $mdDialog.show(confirm).then(function () {
+            assetService.unassignAssetFromCustomer(asset.id.id).then(function success() {
+                vm.grid.refreshList();
+            });
+        });
+    }
+
+    function unassignAssetsFromCustomer($event, items) {
+        var confirm = $mdDialog.confirm()
+            .targetEvent($event)
+            .title($translate.instant('asset.unassign-assets-title', {count: items.selectedCount}, 'messageformat'))
+            .htmlContent($translate.instant('asset.unassign-assets-text'))
+            .ariaLabel($translate.instant('asset.unassign-asset'))
+            .cancel($translate.instant('action.no'))
+            .ok($translate.instant('action.yes'));
+        $mdDialog.show(confirm).then(function () {
+            var tasks = [];
+            for (var id in items.selections) {
+                tasks.push(assetService.unassignAssetFromCustomer(id));
+            }
+            $q.all(tasks).then(function () {
+                vm.grid.refreshList();
+            });
+        });
+    }
+
+    function makePublic($event, asset) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var confirm = $mdDialog.confirm()
+            .targetEvent($event)
+            .title($translate.instant('asset.make-public-asset-title', {assetName: asset.name}))
+            .htmlContent($translate.instant('asset.make-public-asset-text'))
+            .ariaLabel($translate.instant('asset.make-public'))
+            .cancel($translate.instant('action.no'))
+            .ok($translate.instant('action.yes'));
+        $mdDialog.show(confirm).then(function () {
+            assetService.makeAssetPublic(asset.id.id).then(function success() {
+                vm.grid.refreshList();
+            });
+        });
+    }
+}
diff --git a/ui/src/app/asset/asset.directive.js b/ui/src/app/asset/asset.directive.js
new file mode 100644
index 0000000..8c13082
--- /dev/null
+++ b/ui/src/app/asset/asset.directive.js
@@ -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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import assetFieldsetTemplate from './asset-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AssetDirective($compile, $templateCache, toast, $translate, types, assetService, customerService) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(assetFieldsetTemplate);
+        element.html(template);
+
+        scope.isAssignedToCustomer = false;
+        scope.isPublic = false;
+        scope.assignedCustomer = null;
+
+        scope.$watch('asset', function(newVal) {
+            if (newVal) {
+                if (scope.asset.customerId && scope.asset.customerId.id !== types.id.nullUid) {
+                    scope.isAssignedToCustomer = true;
+                    customerService.getShortCustomerInfo(scope.asset.customerId.id).then(
+                        function success(customer) {
+                            scope.assignedCustomer = customer;
+                            scope.isPublic = customer.isPublic;
+                        }
+                    );
+                } else {
+                    scope.isAssignedToCustomer = false;
+                    scope.isPublic = false;
+                    scope.assignedCustomer = null;
+                }
+            }
+        });
+
+        scope.onAssetIdCopied = function() {
+            toast.showSuccess($translate.instant('asset.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
+
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            asset: '=',
+            isEdit: '=',
+            assetScope: '=',
+            theForm: '=',
+            onAssignToCustomer: '&',
+            onMakePublic: '&',
+            onUnassignFromCustomer: '&',
+            onDeleteAsset: '&'
+        }
+    };
+}
diff --git a/ui/src/app/asset/asset.routes.js b/ui/src/app/asset/asset.routes.js
new file mode 100644
index 0000000..c9a312d
--- /dev/null
+++ b/ui/src/app/asset/asset.routes.js
@@ -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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import assetsTemplate from './assets.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function AssetRoutes($stateProvider) {
+    $stateProvider
+        .state('home.assets', {
+            url: '/assets',
+            params: {'topIndex': 0},
+            module: 'private',
+            auth: ['TENANT_ADMIN', 'CUSTOMER_USER'],
+            views: {
+                "content@home": {
+                    templateUrl: assetsTemplate,
+                    controller: 'AssetController',
+                    controllerAs: 'vm'
+                }
+            },
+            data: {
+                assetsType: 'tenant',
+                searchEnabled: true,
+                pageTitle: 'asset.assets'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "domain", "label": "asset.assets"}'
+            }
+        })
+        .state('home.customers.assets', {
+            url: '/:customerId/assets',
+            params: {'topIndex': 0},
+            module: 'private',
+            auth: ['TENANT_ADMIN'],
+            views: {
+                "content@home": {
+                    templateUrl: assetsTemplate,
+                    controllerAs: 'vm',
+                    controller: 'AssetController'
+                }
+            },
+            data: {
+                assetsType: 'customer',
+                searchEnabled: true,
+                pageTitle: 'customer.assets'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "domain", "label": "{{ vm.customerAssetsTitle }}", "translate": "false"}'
+            }
+        });
+
+}
diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html
new file mode 100644
index 0000000..3c06558
--- /dev/null
+++ b/ui/src/app/asset/asset-card.tpl.html
@@ -0,0 +1,19 @@
+<!--
+
+    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 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>
diff --git a/ui/src/app/asset/asset-fieldset.tpl.html b/ui/src/app/asset/asset-fieldset.tpl.html
new file mode 100644
index 0000000..8cf0c96
--- /dev/null
+++ b/ui/src/app/asset/asset-fieldset.tpl.html
@@ -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.
+
+-->
+<md-button ng-click="onMakePublic({event: $event})"
+           ng-show="!isEdit && assetScope === 'tenant' && !isAssignedToCustomer && !isPublic"
+           class="md-raised md-primary">{{ 'asset.make-public' | translate }}</md-button>
+<md-button ng-click="onAssignToCustomer({event: $event})"
+           ng-show="!isEdit && assetScope === 'tenant' && !isAssignedToCustomer"
+           class="md-raised md-primary">{{ 'asset.assign-to-customer' | translate }}</md-button>
+<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
+           ng-show="!isEdit && (assetScope === 'customer' || assetScope === 'tenant') && isAssignedToCustomer"
+           class="md-raised md-primary">{{ isPublic ? 'asset.make-private' : 'asset.unassign-from-customer' | translate }}</md-button>
+<md-button ng-click="onDeleteAsset({event: $event})"
+           ng-show="!isEdit && assetScope === 'tenant'"
+           class="md-raised md-primary">{{ 'asset.delete' | translate }}</md-button>
+
+<div layout="row">
+    <md-button ngclipboard data-clipboard-action="copy"
+               ngclipboard-success="onAssetIdCopied(e)"
+               data-clipboard-text="{{asset.id.id}}" ng-show="!isEdit"
+               class="md-raised">
+        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+        <span translate>asset.copyId</span>
+    </md-button>
+</div>
+
+<md-content class="md-padding" layout="column">
+    <md-input-container class="md-block"
+                        ng-show="!isEdit && isAssignedToCustomer && !isPublic && assetScope === 'tenant'">
+        <label translate>asset.assignedToCustomer</label>
+        <input ng-model="assignedCustomer.title" disabled>
+    </md-input-container>
+    <div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
+         ng-show="!isEdit && isPublic && (assetScope === 'customer' || assetScope === 'tenant')">
+        {{ 'asset.asset-public' | translate }}
+    </div>
+    <fieldset ng-disabled="loading || !isEdit">
+        <md-input-container class="md-block">
+            <label translate>asset.name</label>
+            <input required name="name" ng-model="asset.name">
+            <div ng-messages="theForm.name.$error">
+                <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>
+        <md-input-container class="md-block">
+            <label translate>asset.description</label>
+            <textarea ng-model="asset.additionalInfo.description" rows="2"></textarea>
+        </md-input-container>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html
new file mode 100644
index 0000000..fb3cf56
--- /dev/null
+++ b/ui/src/app/asset/assets.tpl.html
@@ -0,0 +1,58 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<tb-grid grid-configuration="vm.assetGridConfig">
+    <details-buttons tb-help="'assets'" help-container-id="help-container">
+        <div id="help-container"></div>
+    </details-buttons>
+    <md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+             id="tabs" md-border-bottom flex class="tb-absolute-fill">
+        <md-tab label="{{ 'asset.details' | translate }}">
+            <tb-asset  asset="vm.grid.operatingItem()"
+                       is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+                       asset-scope="vm.assetsScope"
+                       the-form="vm.grid.detailsForm"
+                       on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
+                       on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
+                       on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
+                       on-delete-asset="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-asset>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.asset}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.asset}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+                                disable-attribute-scope-selection="true">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'asset.events' | translate }}">
+            <tb-event-table flex entity-type="vm.types.entityType.asset"
+                            entity-id="vm.grid.operatingItem().id.id"
+                            tenant-id="vm.grid.operatingItem().tenantId.id"
+                            default-event-type="{{vm.types.eventType.alarm.value}}">
+            </tb-event-table>
+        </md-tab>
+</tb-grid>
diff --git a/ui/src/app/asset/assign-to-customer.controller.js b/ui/src/app/asset/assign-to-customer.controller.js
new file mode 100644
index 0000000..602e599
--- /dev/null
+++ b/ui/src/app/asset/assign-to-customer.controller.js
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+/*@ngInject*/
+export default function AssignAssetToCustomerController(customerService, assetService, $mdDialog, $q, assetIds, customers) {
+
+    var vm = this;
+
+    vm.customers = customers;
+    vm.searchText = '';
+
+    vm.assign = assign;
+    vm.cancel = cancel;
+    vm.isCustomerSelected = isCustomerSelected;
+    vm.hasData = hasData;
+    vm.noData = noData;
+    vm.searchCustomerTextUpdated = searchCustomerTextUpdated;
+    vm.toggleCustomerSelection = toggleCustomerSelection;
+
+    vm.theCustomers = {
+        getItemAtIndex: function (index) {
+            if (index > vm.customers.data.length) {
+                vm.theCustomers.fetchMoreItems_(index);
+                return null;
+            }
+            var item = vm.customers.data[index];
+            if (item) {
+                item.indexNumber = index + 1;
+            }
+            return item;
+        },
+
+        getLength: function () {
+            if (vm.customers.hasNext) {
+                return vm.customers.data.length + vm.customers.nextPageLink.limit;
+            } else {
+                return vm.customers.data.length;
+            }
+        },
+
+        fetchMoreItems_: function () {
+            if (vm.customers.hasNext && !vm.customers.pending) {
+                vm.customers.pending = true;
+                customerService.getCustomers(vm.customers.nextPageLink).then(
+                    function success(customers) {
+                        vm.customers.data = vm.customers.data.concat(customers.data);
+                        vm.customers.nextPageLink = customers.nextPageLink;
+                        vm.customers.hasNext = customers.hasNext;
+                        if (vm.customers.hasNext) {
+                            vm.customers.nextPageLink.limit = vm.customers.pageSize;
+                        }
+                        vm.customers.pending = false;
+                    },
+                    function fail() {
+                        vm.customers.hasNext = false;
+                        vm.customers.pending = false;
+                    });
+            }
+        }
+    };
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function assign() {
+        var tasks = [];
+        for (var assetId in assetIds) {
+            tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[assetId]));
+        }
+        $q.all(tasks).then(function () {
+            $mdDialog.hide();
+        });
+    }
+
+    function noData() {
+        return vm.customers.data.length == 0 && !vm.customers.hasNext;
+    }
+
+    function hasData() {
+        return vm.customers.data.length > 0;
+    }
+
+    function toggleCustomerSelection($event, customer) {
+        $event.stopPropagation();
+        if (vm.isCustomerSelected(customer)) {
+            vm.customers.selection = null;
+        } else {
+            vm.customers.selection = customer;
+        }
+    }
+
+    function isCustomerSelected(customer) {
+        return vm.customers.selection != null && customer &&
+            customer.id.id === vm.customers.selection.id.id;
+    }
+
+    function searchCustomerTextUpdated() {
+        vm.customers = {
+            pageSize: vm.customers.pageSize,
+            data: [],
+            nextPageLink: {
+                limit: vm.customers.pageSize,
+                textSearch: vm.searchText
+            },
+            selection: null,
+            hasNext: true,
+            pending: false
+        };
+    }
+}
diff --git a/ui/src/app/asset/assign-to-customer.tpl.html b/ui/src/app/asset/assign-to-customer.tpl.html
new file mode 100644
index 0000000..fba56ce
--- /dev/null
+++ b/ui/src/app/asset/assign-to-customer.tpl.html
@@ -0,0 +1,76 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'asset.assign-asset-to-customer' | translate }}">
+    <form name="theForm" ng-submit="vm.assign()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>asset.assign-asset-to-customer</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <fieldset>
+                    <span translate>asset.assign-to-customer-text</span>
+                    <md-input-container class="md-block" style='margin-bottom: 0px;'>
+                        <label>&nbsp;</label>
+                        <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">
+                            search
+                        </md-icon>
+                        <input id="customer-search" autofocus ng-model="vm.searchText"
+                               ng-change="vm.searchCustomerTextUpdated()"
+                               placeholder="{{ 'common.enter-search' | translate }}"/>
+                    </md-input-container>
+                    <div style='min-height: 150px;'>
+					<span translate layout-align="center center"
+                          style="text-transform: uppercase; display: flex; height: 150px;"
+                          class="md-subhead"
+                          ng-show="vm.noData()">customer.no-customers-text</span>
+                        <md-virtual-repeat-container ng-show="vm.hasData()"
+                                                     tb-scope-element="repeatContainer" md-top-index="vm.topIndex" flex
+                                                     style='min-height: 150px; width: 100%;'>
+                            <md-list>
+                                <md-list-item md-virtual-repeat="customer in vm.theCustomers" md-on-demand
+                                              class="repeated-item" flex>
+                                    <md-checkbox ng-click="vm.toggleCustomerSelection($event, customer)"
+                                                 aria-label="{{ 'item.selected' | translate }}"
+                                                 ng-checked="vm.isCustomerSelected(customer)"></md-checkbox>
+                                    <span> {{ customer.title }} </span>
+                                </md-list-item>
+                            </md-list>
+                        </md-virtual-repeat-container>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || vm.customers.selection==null" type="submit" class="md-raised md-primary">
+                {{ 'action.assign' | translate }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/asset/index.js b/ui/src/app/asset/index.js
new file mode 100644
index 0000000..62ab201
--- /dev/null
+++ b/ui/src/app/asset/index.js
@@ -0,0 +1,43 @@
+/*
+ * 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 uiRouter from 'angular-ui-router';
+import thingsboardGrid from '../components/grid.directive';
+import thingsboardEvent from '../event';
+import thingsboardApiUser from '../api/user.service';
+import thingsboardApiAsset from '../api/asset.service';
+import thingsboardApiCustomer from '../api/customer.service';
+
+import AssetRoutes from './asset.routes';
+import {AssetController, AssetCardController} from './asset.controller';
+import AssignAssetToCustomerController from './assign-to-customer.controller';
+import AddAssetsToCustomerController from './add-assets-to-customer.controller';
+import AssetDirective from './asset.directive';
+
+export default angular.module('thingsboard.asset', [
+    uiRouter,
+    thingsboardGrid,
+    thingsboardEvent,
+    thingsboardApiUser,
+    thingsboardApiAsset,
+    thingsboardApiCustomer
+])
+    .config(AssetRoutes)
+    .controller('AssetController', AssetController)
+    .controller('AssetCardController', AssetCardController)
+    .controller('AssignAssetToCustomerController', AssignAssetToCustomerController)
+    .controller('AddAssetsToCustomerController', AddAssetsToCustomerController)
+    .directive('tbAsset', AssetDirective)
+    .name;
diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js
new file mode 100644
index 0000000..0bc73d1
--- /dev/null
+++ b/ui/src/app/common/dashboard-utils.service.js
@@ -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.
+ */
+
+export default angular.module('thingsboard.dashboardUtils', [])
+    .factory('dashboardUtils', DashboardUtils)
+    .name;
+
+/*@ngInject*/
+function DashboardUtils(types, timeService) {
+
+    var service = {
+        validateAndUpdateDashboard: validateAndUpdateDashboard
+    };
+
+    return service;
+
+    function validateAndUpdateEntityAliases(configuration) {
+        if (angular.isUndefined(configuration.entityAliases)) {
+            configuration.entityAliases = {};
+            if (configuration.deviceAliases) {
+                var deviceAliases = configuration.deviceAliases;
+                for (var aliasId in deviceAliases) {
+                    var deviceAlias = deviceAliases[aliasId];
+                    var alias = deviceAlias.alias;
+                    var entityFilter = {
+                        useFilter: false,
+                        entityNameFilter: '',
+                        entityList: []
+                    }
+                    if (deviceAlias.deviceFilter) {
+                        entityFilter.useFilter = deviceAlias.deviceFilter.useFilter;
+                        entityFilter.entityNameFilter = deviceAlias.deviceFilter.deviceNameFilter;
+                        entityFilter.entityList = deviceAlias.deviceFilter.deviceList;
+                    } else if (deviceAlias.deviceId) {
+                        entityFilter.entityList = [deviceAlias.deviceId];
+                    }
+                    var entityAlias = {
+                        id: aliasId,
+                        alias: alias,
+                        entityType: types.entityType.device,
+                        entityFilter: entityFilter
+                    };
+                    configuration.entityAliases[aliasId] = entityAlias;
+                }
+                delete configuration.deviceAliases;
+            }
+        }
+        return configuration;
+    }
+
+    function validateAndUpdateWidget(widget) {
+        if (!widget.config) {
+            widget.config = {};
+        }
+        if (!widget.config.datasources) {
+            widget.config.datasources = [];
+        }
+        widget.config.datasources.forEach(function(datasource) {
+             if (datasource.type === 'device') {
+                 datasource.type = types.datasourceType.entity;
+             }
+             if (datasource.deviceAliasId) {
+                 datasource.entityAliasId = datasource.deviceAliasId;
+                 delete datasource.deviceAliasId;
+             }
+        });
+    }
+
+    function validateAndUpdateDashboard(dashboard) {
+        if (!dashboard.configuration) {
+            dashboard.configuration = {
+                widgets: [],
+                entityAliases: {}
+            };
+        }
+        if (angular.isUndefined(dashboard.configuration.widgets)) {
+            dashboard.configuration.widgets = [];
+        }
+        dashboard.configuration.widgets.forEach(function(widget) {
+            validateAndUpdateWidget(widget);
+        });
+        if (angular.isUndefined(dashboard.configuration.timewindow)) {
+            dashboard.configuration.timewindow = timeService.defaultTimewindow();
+        }
+        if (angular.isDefined(dashboard.configuration.gridSettings)) {
+            if (angular.isDefined(dashboard.configuration.gridSettings.showDevicesSelect)) {
+                dashboard.configuration.gridSettings.showEntitiesSelect = dashboard.configuration.gridSettings.showDevicesSelect;
+                delete dashboard.configuration.gridSettings.showDevicesSelect;
+            }
+        }
+        dashboard.configuration = validateAndUpdateEntityAliases(dashboard.configuration);
+        return dashboard;
+    }
+}
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index df321f7..ae3556f 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -79,7 +79,7 @@ export default angular.module('thingsboard.types', [])
             },
             datasourceType: {
                 function: "function",
-                device: "device"
+                entity: "entity"
             },
             dataKeyType: {
                 timeseries: "timeseries",
@@ -93,11 +93,12 @@ export default angular.module('thingsboard.types', [])
                 plugin: "PLUGIN"
             },
             entityType: {
-                tenant: "TENANT",
                 device: "DEVICE",
-                customer: "CUSTOMER",
+                asset: "ASSET",
                 rule: "RULE",
-                plugin: "PLUGIN"
+                plugin: "PLUGIN",
+                tenant: "TENANT",
+                customer: "CUSTOMER"
             },
             eventType: {
                 alarm: {
@@ -122,7 +123,7 @@ export default angular.module('thingsboard.types', [])
                 name: "attribute.scope-latest-telemetry",
                 clientSide: true
             },
-            deviceAttributesScope: {
+            attributesScope: {
                 client: {
                     value: "CLIENT_SCOPE",
                     name: "attribute.scope-client",
diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js
index 3fc5202..b324cbf 100644
--- a/ui/src/app/common/utils.service.js
+++ b/ui/src/app/common/utils.service.js
@@ -104,7 +104,11 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
         parseException: parseException,
         processWidgetException: processWidgetException,
         isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
-        filterSearchTextEntities: filterSearchTextEntities
+        filterSearchTextEntities: filterSearchTextEntities,
+        guid: guid,
+        isLocalUrl: isLocalUrl,
+        validateDatasources: validateDatasources,
+        createKey: createKey
     }
 
     return service;
@@ -276,4 +280,70 @@ function Utils($mdColorPalette, $rootScope, $window, types) {
         deferred.resolve(response);
     }
 
+    function guid() {
+        function s4() {
+            return Math.floor((1 + Math.random()) * 0x10000)
+                .toString(16)
+                .substring(1);
+        }
+        return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+            s4() + '-' + s4() + s4() + s4();
+    }
+
+    function genNextColor(datasources) {
+        var index = 0;
+        if (datasources) {
+            for (var i = 0; i < datasources.length; i++) {
+                var datasource = datasources[i];
+                index += datasource.dataKeys.length;
+            }
+        }
+        return getMaterialColor(index);
+    }
+
+    function isLocalUrl(url) {
+        var parser = document.createElement('a'); //eslint-disable-line
+        parser.href = url;
+        var host = parser.hostname;
+        if (host === "localhost" || host === "127.0.0.1") {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    function validateDatasources(datasources) {
+        datasources.forEach(function (datasource) {
+            if (datasource.type === 'device') {
+                datasource.type = types.datasourceType.entity;
+                datasource.entityType = types.entityType.device;
+                if (datasource.deviceId) {
+                    datasource.entityId = datasource.deviceId;
+                } else if (datasource.deviceAliasId) {
+                    datasource.entityAliasId = datasource.deviceAliasId;
+                }
+                if (datasource.deviceName) {
+                    datasource.entityName = datasource.deviceName;
+                }
+            }
+            if (datasource.type === types.datasourceType.entity && datasource.entityId) {
+                datasource.name = datasource.entityName;
+            }
+        });
+        return datasources;
+    }
+
+    function createKey(keyInfo, type, datasources) {
+        var dataKey = {
+            name: keyInfo.name,
+            type: type,
+            label: keyInfo.label || keyInfo.name,
+            color: genNextColor(datasources),
+            funcBody: keyInfo.funcBody,
+            settings: {},
+            _hash: Math.random()
+        }
+        return dataKey;
+    }
+
 }
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index 7c7a552..93b889a 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -182,12 +182,14 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
 
     vm.dashboardTimewindowApi = {
         onResetTimewindow: function() {
-            if (vm.originalDashboardTimewindow) {
+            $timeout(function() {
+                if (vm.originalDashboardTimewindow) {
                 $timeout(function() {
                     vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
                     vm.originalDashboardTimewindow = null;
                 }, 0);
-            }
+                }
+            }, 0);
         },
         onUpdateTimewindow: function(startTimeMs, endTimeMs) {
             if (!vm.originalDashboardTimewindow) {
@@ -272,8 +274,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, t
         $scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
     });
 
-    $scope.$watch('vm.aliasesInfo.deviceAliases', function () {
-        $scope.$broadcast('deviceAliasListChanged', vm.aliasesInfo);
+    $scope.$watch('vm.aliasesInfo.entityAliases', function () {
+        $scope.$broadcast('entityAliasListChanged', vm.aliasesInfo);
     }, true);
 
     $scope.$on('gridster-resized', function (event, sizes, theGridster) {
diff --git a/ui/src/app/components/datakey-config.directive.js b/ui/src/app/components/datakey-config.directive.js
index 50bbfcb..9b3081e 100644
--- a/ui/src/app/components/datakey-config.directive.js
+++ b/ui/src/app/components/datakey-config.directive.js
@@ -108,9 +108,9 @@ function DatakeyConfig($compile, $templateCache, $q, types) {
         }, true);
 
         scope.keysSearch = function (searchText) {
-            if (scope.deviceAlias) {
+            if (scope.entityAlias) {
                 var deferred = $q.defer();
-                scope.fetchDeviceKeys({deviceAliasId: scope.deviceAlias.id, query: searchText, type: scope.model.type})
+                scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: scope.model.type})
                     .then(function (keys) {
                         keys.push(searchText);
                         deferred.resolve(keys);
@@ -130,8 +130,8 @@ function DatakeyConfig($compile, $templateCache, $q, types) {
         restrict: 'E',
         require: '^ngModel',
         scope: {
-            deviceAlias: '=',
-            fetchDeviceKeys: '&',
+            entityAlias: '=',
+            fetchEntityKeys: '&',
             datakeySettingsSchema: '='
         },
         link: linker
diff --git a/ui/src/app/components/datakey-config-dialog.controller.js b/ui/src/app/components/datakey-config-dialog.controller.js
index 9bdaae2..a8741f5 100644
--- a/ui/src/app/components/datakey-config-dialog.controller.js
+++ b/ui/src/app/components/datakey-config-dialog.controller.js
@@ -20,14 +20,14 @@ export default angular.module('thingsboard.dialogs.datakeyConfigDialog', [things
     .name;
 
 /*@ngInject*/
-function DatakeyConfigDialogController($scope, $mdDialog, deviceService, dataKey, dataKeySettingsSchema, deviceAlias, deviceAliases) {
+function DatakeyConfigDialogController($scope, $mdDialog, entityService, dataKey, dataKeySettingsSchema, entityAlias, entityAliases) {
 
     var vm = this;
 
     vm.dataKey = dataKey;
     vm.dataKeySettingsSchema = dataKeySettingsSchema;
-    vm.deviceAlias = deviceAlias;
-    vm.deviceAliases = deviceAliases;
+    vm.entityAlias = entityAlias;
+    vm.entityAliases = entityAliases;
 
     vm.hide = function () {
         $mdDialog.hide();
@@ -37,10 +37,10 @@ function DatakeyConfigDialogController($scope, $mdDialog, deviceService, dataKey
         $mdDialog.cancel();
     };
 
-    vm.fetchDeviceKeys = function (deviceAliasId, query, type) {
-        var alias = vm.deviceAliases[deviceAliasId];
+    vm.fetchEntityKeys = function (entityAliasId, query, type) {
+        var alias = vm.entityAliases[entityAliasId];
         if (alias) {
-            return deviceService.getDeviceKeys(alias.deviceId, query, type);
+            return entityService.getEntityKeys(alias.entityType, alias.entityId, query, type);
         } else {
             return [];
         }
diff --git a/ui/src/app/components/datakey-config-dialog.tpl.html b/ui/src/app/components/datakey-config-dialog.tpl.html
index e827238..a55c316 100644
--- a/ui/src/app/components/datakey-config-dialog.tpl.html
+++ b/ui/src/app/components/datakey-config-dialog.tpl.html
@@ -30,8 +30,8 @@
   	    <span style="min-height: 5px;" flex="" ng-show="!loading"></span>	    
 	    <md-dialog-content>
 			<tb-datakey-config ng-model="vm.dataKey"
-                               fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
-                               device-alias="vm.deviceAlias"
+                               fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)"
+                               entity-alias="vm.entityAlias"
                                datakey-settings-schema="vm.dataKeySettingsSchema">
 			</tb-datakey-config>
 	    </md-dialog-content>
diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js
index 82e8cdd..2c06a38 100644
--- a/ui/src/app/components/datasource.directive.js
+++ b/ui/src/app/components/datasource.directive.js
@@ -17,7 +17,7 @@ import './datasource.scss';
 
 import thingsboardTypes from '../common/types.constant';
 import thingsboardDatasourceFunc from './datasource-func.directive'
-import thingsboardDatasourceDevice from './datasource-device.directive';
+import thingsboardDatasourceEntity from './datasource-entity.directive';
 
 /* eslint-disable import/no-unresolved, import/default */
 
@@ -25,7 +25,7 @@ import datasourceTemplate from './datasource.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
-export default angular.module('thingsboard.directives.datasource', [thingsboardTypes, thingsboardDatasourceFunc, thingsboardDatasourceDevice])
+export default angular.module('thingsboard.directives.datasource', [thingsboardTypes, thingsboardDatasourceFunc, thingsboardDatasourceEntity])
     .directive('tbDatasource', Datasource)
     .name;
 
@@ -42,7 +42,7 @@ function Datasource($compile, $templateCache, types) {
         if (scope.functionsOnly) {
             scope.datasourceTypes = [types.datasourceType.function];
         } else{
-            scope.datasourceTypes = [types.datasourceType.device, types.datasourceType.function];
+            scope.datasourceTypes = [types.datasourceType.entity, types.datasourceType.function];
         }
 
         scope.updateView = function () {
@@ -76,13 +76,13 @@ function Datasource($compile, $templateCache, types) {
         restrict: "E",
         require: "^ngModel",
         scope: {
-            deviceAliases: '=',
+            entityAliases: '=',
             widgetType: '=',
             functionsOnly: '=',
             datakeySettingsSchema: '=',
             generateDataKey: '&',
-            fetchDeviceKeys: '&',
-            onCreateDeviceAlias: '&'
+            fetchEntityKeys: '&',
+            onCreateEntityAlias: '&'
         },
         link: linker
     };
diff --git a/ui/src/app/components/datasource.scss b/ui/src/app/components/datasource.scss
index 02bbbff..f188a04 100644
--- a/ui/src/app/components/datasource.scss
+++ b/ui/src/app/components/datasource.scss
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 .tb-datasource {
-   #device-autocomplete {
+   #entity-autocomplete {
      height: 30px;
      margin-top: 18px;
      md-autocomplete-wrap {
diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html
index cbd638c..88ff247 100644
--- a/ui/src/app/components/datasource.tpl.html
+++ b/ui/src/app/components/datasource.tpl.html
@@ -31,16 +31,16 @@
                             ng-required="model.type === types.datasourceType.function"
                             generate-data-key="generateDataKey({chip: chip, type: type})">
         </tb-datasource-func>
-        <tb-datasource-device flex
+        <tb-datasource-entity flex
                               ng-model="model"
                               datakey-settings-schema="datakeySettingsSchema"
-                              ng-switch-when="device"
-                              ng-required="model.type === types.datasourceType.device"
+                              ng-switch-when="entity"
+                              ng-required="model.type === types.datasourceType.entity"
                               widget-type="widgetType"
-                              device-aliases="deviceAliases"
+                              entity-aliases="entityAliases"
                               generate-data-key="generateDataKey({chip: chip, type: type})"
-                              fetch-device-keys="fetchDeviceKeys({deviceAliasId: deviceAliasId, query: query, type: type})"
-                              on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
-        </tb-datasource-device>
+                              fetch-entity-keys="fetchEntityKeys({entityAliasId: entityAliasId, query: query, type: type})"
+                              on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})">
+        </tb-datasource-entity>
     </section>
 </section>
diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js
new file mode 100644
index 0000000..97732b9
--- /dev/null
+++ b/ui/src/app/components/datasource-entity.directive.js
@@ -0,0 +1,249 @@
+/*
+ * 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 './datasource-entity.scss';
+
+import 'md-color-picker';
+import tinycolor from 'tinycolor2';
+import $ from 'jquery';
+import thingsboardTypes from '../common/types.constant';
+import thingsboardDatakeyConfigDialog from './datakey-config-dialog.controller';
+import thingsboardTruncate from './truncate.filter';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import datasourceEntityTemplate from './datasource-entity.tpl.html';
+import datakeyConfigDialogTemplate from './datakey-config-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.datasourceEntity', [thingsboardTruncate, thingsboardTypes, thingsboardDatakeyConfigDialog])
+    .directive('tbDatasourceEntity', DatasourceEntity)
+    .name;
+
+/*@ngInject*/
+function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $document, $mdColorPicker, $mdConstant, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(datasourceEntityTemplate);
+        element.html(template);
+
+        scope.ngModelCtrl = ngModelCtrl;
+        scope.types = types;
+
+        scope.selectedTimeseriesDataKey = null;
+        scope.timeseriesDataKeySearchText = null;
+
+        scope.selectedAttributeDataKey = null;
+        scope.attributeDataKeySearchText = null;
+
+        scope.updateValidity = function () {
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                var dataValid = angular.isDefined(value) && value != null;
+                ngModelCtrl.$setValidity('entityData', dataValid);
+                if (dataValid) {
+                    ngModelCtrl.$setValidity('entityAlias',
+                        angular.isDefined(value.entityAliasId) &&
+                        value.entityAliasId != null);
+                    ngModelCtrl.$setValidity('entityKeys',
+                        angular.isDefined(value.dataKeys) &&
+                        value.dataKeys != null &&
+                        value.dataKeys.length > 0);
+                }
+            }
+        };
+
+        scope.$watch('entityAlias', function () {
+            if (ngModelCtrl.$viewValue) {
+                if (scope.entityAlias) {
+                    ngModelCtrl.$viewValue.entityAliasId = scope.entityAlias.id;
+                } else {
+                    ngModelCtrl.$viewValue.entityAliasId = null;
+                }
+                scope.updateValidity();
+                scope.selectedEntityAliasChange();
+            }
+        });
+
+        scope.$watch('timeseriesDataKeys', function () {
+            if (ngModelCtrl.$viewValue) {
+                var dataKeys = [];
+                dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
+                dataKeys = dataKeys.concat(scope.attributeDataKeys);
+                ngModelCtrl.$viewValue.dataKeys = dataKeys;
+                scope.updateValidity();
+            }
+        }, true);
+
+        scope.$watch('attributeDataKeys', function () {
+            if (ngModelCtrl.$viewValue) {
+                var dataKeys = [];
+                dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
+                dataKeys = dataKeys.concat(scope.attributeDataKeys);
+                ngModelCtrl.$viewValue.dataKeys = dataKeys;
+                scope.updateValidity();
+            }
+        }, true);
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                var entityAliasId = ngModelCtrl.$viewValue.entityAliasId;
+                if (scope.entityAliases[entityAliasId]) {
+                    scope.entityAlias = {id: entityAliasId, alias: scope.entityAliases[entityAliasId].alias,
+                        entityType: scope.entityAliases[entityAliasId].entityType,
+                        entityId: scope.entityAliases[entityAliasId].entityId};
+                } else {
+                    scope.entityAlias = null;
+                }
+                var timeseriesDataKeys = [];
+                var attributeDataKeys = [];
+                for (var d in ngModelCtrl.$viewValue.dataKeys) {
+                    var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
+                    if (dataKey.type === types.dataKeyType.timeseries) {
+                        timeseriesDataKeys.push(dataKey);
+                    } else if (dataKey.type === types.dataKeyType.attribute) {
+                        attributeDataKeys.push(dataKey);
+                    }
+                }
+                scope.timeseriesDataKeys = timeseriesDataKeys;
+                scope.attributeDataKeys = attributeDataKeys;
+            }
+        };
+
+        scope.textIsNotEmpty = function(text) {
+            return (text && text != null && text.length > 0) ? true : false;
+        }
+
+        scope.selectedEntityAliasChange = function () {
+            if (!scope.timeseriesDataKeySearchText || scope.timeseriesDataKeySearchText === '') {
+                scope.timeseriesDataKeySearchText = scope.timeseriesDataKeySearchText === '' ? null : '';
+            }
+            if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') {
+                scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : '';
+            }
+        };
+
+        scope.transformTimeseriesDataKeyChip = function (chip) {
+            return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries});
+        };
+
+        scope.transformAttributeDataKeyChip = function (chip) {
+            return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
+        };
+
+        scope.showColorPicker = function (event, dataKey) {
+            $mdColorPicker.show({
+                value: dataKey.color,
+                defaultValue: '#fff',
+                random: tinycolor.random(),
+                clickOutsideToClose: false,
+                hasBackdrop: false,
+                skipHide: true,
+                preserveScope: false,
+
+                mdColorAlphaChannel: true,
+                mdColorSpectrum: true,
+                mdColorSliders: true,
+                mdColorGenericPalette: false,
+                mdColorMaterialPalette: true,
+                mdColorHistory: false,
+                mdColorDefaultTab: 2,
+
+                $event: event
+
+            }).then(function (color) {
+                dataKey.color = color;
+                ngModelCtrl.$setDirty();
+            });
+        }
+
+        scope.editDataKey = function (event, dataKey, index) {
+
+            $mdDialog.show({
+                controller: 'DatakeyConfigDialogController',
+                controllerAs: 'vm',
+                templateUrl: datakeyConfigDialogTemplate,
+                locals: {
+                    dataKey: angular.copy(dataKey),
+                    dataKeySettingsSchema: scope.datakeySettingsSchema,
+                    entityAlias: scope.entityAlias,
+                    entityAliases: scope.entityAliases
+                },
+                parent: angular.element($document[0].body),
+                fullscreen: true,
+                targetEvent: event,
+                skipHide: true,
+                onComplete: function () {
+                    var w = angular.element($window);
+                    w.triggerHandler('resize');
+                }
+            }).then(function (dataKey) {
+                if (dataKey.type === types.dataKeyType.timeseries) {
+                    scope.timeseriesDataKeys[index] = dataKey;
+                } else if (dataKey.type === types.dataKeyType.attribute) {
+                    scope.attributeDataKeys[index] = dataKey;
+                }
+                ngModelCtrl.$setDirty();
+            }, function () {
+            });
+        };
+
+        scope.dataKeysSearch = function (searchText, type) {
+            if (scope.entityAlias) {
+                var deferred = $q.defer();
+                scope.fetchEntityKeys({entityAliasId: scope.entityAlias.id, query: searchText, type: type})
+                    .then(function (dataKeys) {
+                        deferred.resolve(dataKeys);
+                    }, function (e) {
+                        deferred.reject(e);
+                    });
+                return deferred.promise;
+            } else {
+                return $q.when([]);
+            }
+        };
+
+        scope.createKey = function (event, chipsId) {
+            var chipsChild = $(chipsId, element)[0].firstElementChild;
+            var el = angular.element(chipsChild);
+            var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer();
+            event.preventDefault();
+            event.stopPropagation();
+            el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim());
+            el.scope().$mdChipsCtrl.resetChipBuffer();
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            widgetType: '=',
+            entityAliases: '=',
+            datakeySettingsSchema: '=',
+            generateDataKey: '&',
+            fetchEntityKeys: '&',
+            onCreateEntityAlias: '&'
+        },
+        link: linker
+    };
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/components/datasource-entity.scss b/ui/src/app/components/datasource-entity.scss
new file mode 100644
index 0000000..7a87fc7
--- /dev/null
+++ b/ui/src/app/components/datasource-entity.scss
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../scss/constants';
+
+.tb-entity-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete {
+  .tb-not-found {
+    display: block;
+    line-height: 1.5;
+    height: 48px;
+    .tb-no-entries {
+      line-height: 48px;
+    }
+  }
+  li {
+    height: auto !important;
+    white-space: normal !important;
+  }
+}
+
+tb-datasource-entity {
+  @media (min-width: $layout-breakpoint-gt-sm) {
+    padding-left: 4px;
+    padding-right: 4px;
+  }
+  tb-entity-alias-select {
+    @media (min-width: $layout-breakpoint-gt-sm) {
+      width: 200px;
+      max-width: 200px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html
new file mode 100644
index 0000000..97ee1b5
--- /dev/null
+++ b/ui/src/app/components/datasource-entity.tpl.html
@@ -0,0 +1,137 @@
+<!--
+
+    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.
+
+-->
+<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
+	   <tb-entity-alias-select
+							  tb-required="true"
+							  entity-aliases="entityAliases"
+							  ng-model="entityAlias"
+							  on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})">
+	   </tb-entity-alias-select>
+	   <section flex layout='column'>
+		   <section flex layout='column' layout-align="center" style="padding-left: 4px;">
+			   <md-chips flex
+						 id="timeseries_datakey_chips"
+						 ng-required="true"
+						 ng-model="timeseriesDataKeys" md-autocomplete-snap
+						 md-transform-chip="transformTimeseriesDataKeyChip($chip)"
+						 md-require-match="false">
+					  <md-autocomplete
+							md-no-cache="true"
+							id="timeseries_datakey"
+							md-selected-item="selectedTimeseriesDataKey"
+							md-search-text="timeseriesDataKeySearchText"
+							md-items="item in dataKeysSearch(timeseriesDataKeySearchText, types.dataKeyType.timeseries)"
+							md-item-text="item.name"
+							md-min-length="0"
+							placeholder="{{'datakey.timeseries' | translate }}"
+							md-menu-class="tb-timeseries-datakey-autocomplete">
+							<span md-highlight-text="timeseriesDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+							<md-not-found>
+							  <div class="tb-not-found">
+								  <div class="tb-no-entries" ng-if="!textIsNotEmpty(timeseriesDataKeySearchText)">
+									  <span translate>entity.no-keys-found</span>
+								  </div>
+								  <div ng-if="textIsNotEmpty(timeseriesDataKeySearchText)">
+									  <span translate translate-values='{ key: "{{timeseriesDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-key-matching</span>
+									  <span>
+											<a translate ng-click="createKey($event, '#timeseries_datakey_chips')">entity.create-new-key</a>
+									  </span>
+								  </div>
+							  </div>
+							</md-not-found>
+					  </md-autocomplete>
+					  <md-chip-template>
+						<div layout="row" layout-align="start center" class="tb-attribute-chip">
+							<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+								<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+							</div>
+							<div layout="row" flex>
+							  <div class="tb-chip-label">
+							  	{{$chip.label}}
+							  </div>
+							  <div class="tb-chip-separator">: </div>
+							  <div class="tb-chip-label">
+								  <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+								  <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+							  </div>
+							</div>
+							<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+								<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+							</md-button>
+						</div>
+					  </md-chip-template>
+			   </md-chips>
+			   <md-chips flex ng-if="widgetType === types.widgetType.latest.value"
+                         id="attribute_datakey_chips"
+                         ng-required="true"
+                         ng-model="attributeDataKeys" md-autocomplete-snap
+                         md-transform-chip="transformAttributeDataKeyChip($chip)"
+                         md-require-match="false">
+					  <md-autocomplete
+							md-no-cache="true"
+							id="attribute_datakey"
+							md-selected-item="selectedAttributeDataKey"
+							md-search-text="attributeDataKeySearchText"
+							md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
+							md-item-text="item.name"
+							md-min-length="0"
+							placeholder="{{'datakey.attributes' | translate }}"
+							md-menu-class="tb-attribute-datakey-autocomplete">
+							<span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
+						    <md-not-found>
+							  <div class="tb-not-found">
+								  <div class="tb-no-entries" ng-if="!textIsNotEmpty(attributeDataKeySearchText)">
+									  <span translate>entity.no-keys-found</span>
+								  </div>
+								  <div ng-if="textIsNotEmpty(attributeDataKeySearchText)">
+									  <span translate translate-values='{ key: "{{attributeDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-key-matching</span>
+									  <span>
+											<a translate ng-click="createKey($event, '#attribute_datakey_chips')">entity.create-new-key</a>
+									  </span>
+								  </div>
+							  </div>
+						    </md-not-found>
+					  </md-autocomplete>
+					  <md-chip-template>
+						  <div layout="row" layout-align="start center" class="tb-attribute-chip">
+							  <div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
+								  <div class="tb-color-result" ng-style="{background: $chip.color}"></div>
+							  </div>
+							  <div layout="row" flex>
+								  <div class="tb-chip-label">
+									  {{$chip.label}}
+								  </div>
+								  <div class="tb-chip-separator">: </div>
+								  <div class="tb-chip-label">
+									  <strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
+									  <strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
+								  </div>
+							  </div>
+							  <md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
+								  <md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
+							  </md-button>
+						  </div>
+					  </md-chip-template>
+			   </md-chips>
+		   </section>
+		   <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+			    <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div>
+				<div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
+			</div>
+	   </section>
+</section>
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
index 5d3c2e7..0f66dca 100644
--- a/ui/src/app/components/datasource-func.directive.js
+++ b/ui/src/app/components/datasource-func.directive.js
@@ -120,8 +120,8 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
                 locals: {
                     dataKey: angular.copy(dataKey),
                     dataKeySettingsSchema: scope.datakeySettingsSchema,
-                    deviceAlias: null,
-                    deviceAliases: null
+                    entityAlias: null,
+                    entityAliases: null
                 },
                 parent: angular.element($document[0].body),
                 fullscreen: true,
diff --git a/ui/src/app/components/device-filter.directive.js b/ui/src/app/components/device-filter.directive.js
index ecf12cd..0233c95 100644
--- a/ui/src/app/components/device-filter.directive.js
+++ b/ui/src/app/components/device-filter.directive.js
@@ -41,7 +41,7 @@ function DeviceFilter($compile, $templateCache, $q, deviceService) {
 
             var deferred = $q.defer();
 
-            deviceService.getTenantDevices(pageLink).then(function success(result) {
+            deviceService.getTenantDevices(pageLink, false).then(function success(result) {
                 deferred.resolve(result.data);
             }, function fail() {
                 deferred.reject();
diff --git a/ui/src/app/components/entity-alias-select.directive.js b/ui/src/app/components/entity-alias-select.directive.js
new file mode 100644
index 0000000..204b83f
--- /dev/null
+++ b/ui/src/app/components/entity-alias-select.directive.js
@@ -0,0 +1,151 @@
+/*
+ * 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 $ from 'jquery';
+
+import './entity-alias-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entityAliasSelectTemplate from './entity-alias-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+/* eslint-disable angular/angularelement */
+
+export default angular.module('thingsboard.directives.entityAliasSelect', [])
+    .directive('tbEntityAliasSelect', EntityAliasSelect)
+    .name;
+
+/*@ngInject*/
+function EntityAliasSelect($compile, $templateCache, $mdConstant) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entityAliasSelectTemplate);
+        element.html(template);
+
+        scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
+
+        scope.ngModelCtrl = ngModelCtrl;
+        scope.entityAliasList = [];
+        scope.entityAlias = null;
+
+        scope.updateValidity = function () {
+            var value = ngModelCtrl.$viewValue;
+            var valid = angular.isDefined(value) && value != null || !scope.tbRequired;
+            ngModelCtrl.$setValidity('entityAlias', valid);
+        };
+
+        scope.$watch('entityAliases', function () {
+            scope.entityAliasList = [];
+            for (var aliasId in scope.entityAliases) {
+                if (scope.allowedEntityTypes) {
+                    if (scope.allowedEntityTypes.indexOf(scope.entityAliases[aliasId].entityType) === -1) {
+                        continue;
+                    }
+                }
+                var entityAlias = {id: aliasId, alias: scope.entityAliases[aliasId].alias,
+                    entityType: scope.entityAliases[aliasId].entityType, entityId: scope.entityAliases[aliasId].entityId};
+                scope.entityAliasList.push(entityAlias);
+            }
+        }, true);
+
+        scope.$watch('entityAlias', function () {
+            scope.updateView();
+        });
+
+        scope.entityAliasSearch = function (entityAliasSearchText) {
+            return entityAliasSearchText ? scope.entityAliasList.filter(
+                scope.createFilterForEntityAlias(entityAliasSearchText)) : scope.entityAliasList;
+        };
+
+        scope.createFilterForEntityAlias = function (query) {
+            var lowercaseQuery = angular.lowercase(query);
+            return function filterFn(entityAlias) {
+                return (angular.lowercase(entityAlias.alias).indexOf(lowercaseQuery) === 0);
+            };
+        };
+
+        scope.updateView = function () {
+            ngModelCtrl.$setViewValue(scope.entityAlias);
+            scope.updateValidity();
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                scope.entityAlias = ngModelCtrl.$viewValue;
+            }
+        }
+
+        scope.textIsNotEmpty = function(text) {
+            return (text && text != null && text.length > 0) ? true : false;
+        }
+
+        scope.entityAliasEnter = function($event) {
+            if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
+                $event.preventDefault();
+                if (!scope.entityAlias) {
+                    var found = scope.entityAliasSearch(scope.entityAliasSearchText);
+                    found = found.length > 0;
+                    if (!found) {
+                        scope.createEntityAlias($event, scope.entityAliasSearchText);
+                    }
+                }
+            }
+        }
+
+        scope.createEntityAlias = function (event, alias) {
+            var autoChild = $('#entity-autocomplete', element)[0].firstElementChild;
+            var el = angular.element(autoChild);
+            el.scope().$mdAutocompleteCtrl.hidden = true;
+            el.scope().$mdAutocompleteCtrl.hasNotFound = false;
+            event.preventDefault();
+            var promise = scope.onCreateEntityAlias({event: event, alias: alias, allowedEntityTypes: scope.allowedEntityTypes});
+            if (promise) {
+                promise.then(
+                    function success(newAlias) {
+                        el.scope().$mdAutocompleteCtrl.hasNotFound = true;
+                        if (newAlias) {
+                            scope.entityAliasList.push(newAlias);
+                            scope.entityAlias = newAlias;
+                        }
+                    },
+                    function fail() {
+                        el.scope().$mdAutocompleteCtrl.hasNotFound = true;
+                    }
+                );
+            } else {
+                el.scope().$mdAutocompleteCtrl.hasNotFound = true;
+            }
+        };
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            tbRequired: '=?',
+            entityAliases: '=',
+            allowedEntityTypes: '=?',
+            onCreateEntityAlias: '&'
+        }
+    };
+}
+
+/* eslint-enable angular/angularelement */
diff --git a/ui/src/app/components/entity-alias-select.scss b/ui/src/app/components/entity-alias-select.scss
new file mode 100644
index 0000000..c23982f
--- /dev/null
+++ b/ui/src/app/components/entity-alias-select.scss
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.tb-entity-alias-autocomplete {
+  .tb-not-found {
+    display: block;
+    line-height: 1.5;
+    height: 48px;
+    .tb-no-entries {
+      line-height: 48px;
+    }
+  }
+  li {
+    height: auto !important;
+    white-space: normal !important;
+  }
+}
diff --git a/ui/src/app/components/entity-alias-select.tpl.html b/ui/src/app/components/entity-alias-select.tpl.html
new file mode 100644
index 0000000..37ca52a
--- /dev/null
+++ b/ui/src/app/components/entity-alias-select.tpl.html
@@ -0,0 +1,53 @@
+<!--
+
+    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.
+
+-->
+<section layout='column'>
+    <md-autocomplete id="entity-autocomplete"
+                     md-input-name="entity_alias"
+                     ng-required="tbRequired"
+                     md-no-cache="true"
+                     ng-model="entityAlias"
+                     md-selected-item="entityAlias"
+                     md-search-text="entityAliasSearchText"
+                     md-items="item in entityAliasSearch(entityAliasSearchText)"
+                     md-item-text="item.alias"
+                     tb-keydown="entityAliasEnter($event)"
+                     tb-keypress="entityAliasEnter($event)"
+                     md-min-length="0"
+                     placeholder="{{ 'entity.entity-alias' | translate }}"
+                     md-menu-class="tb-entity-alias-autocomplete">
+        <md-item-template>
+            <span md-highlight-text="entityAliasSearchText" md-highlight-flags="^i">{{item.alias}}</span>
+        </md-item-template>
+        <md-not-found>
+            <div class="tb-not-found">
+                <div class="tb-no-entries" ng-if="!textIsNotEmpty(entityAliasSearchText)">
+                    <span translate>entity.no-aliases-found</span>
+                </div>
+                <div ng-if="textIsNotEmpty(entityAliasSearchText)">
+                    <span translate translate-values='{ alias: "{{entityAliasSearchText | truncate:true:6:&apos;...&apos;}}" }'>entity.no-alias-matching</span>
+                    <span>
+                        <a translate ng-click="createEntityAlias($event, entityAliasSearchText)">entity.create-new-alias</a>
+                    </span>
+                </div>
+            </div>
+        </md-not-found>
+    </md-autocomplete>
+    <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+        <div translate ng-message="entityAlias" class="tb-error-message">entity.alias-required</div>
+    </div>
+</section>
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
index 1cf0085..c29c2e5 100644
--- a/ui/src/app/components/grid.tpl.html
+++ b/ui/src/app/components/grid.tpl.html
@@ -49,7 +49,7 @@
                                 <md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
                                            ng-click="action.onAction($event, rowItem[n])" aria-label="{{ action.name() }}">
                                     <md-tooltip md-direction="top">
-                                        {{ action.details() }}
+                                        {{ action.details( rowItem[n] ) }}
                                     </md-tooltip>
                                     <ng-md-icon icon="{{action.icon}}"></ng-md-icon>
                                 </md-button>
@@ -62,7 +62,7 @@
     </div>
     <tb-details-sidenav
             header-title="{{vm.getItemTitleFunc(vm.operatingItem())}}"
-            header-subtitle="{{vm.itemDetailsText()}}"
+            header-subtitle="{{vm.itemDetailsText(vm.operatingItem())}}"
             is-read-only="vm.isDetailsReadOnly(vm.operatingItem())"
             is-open="vm.detailsConfig.isDetailsOpen"
             is-edit="vm.detailsConfig.isDetailsEditMode"
diff --git a/ui/src/app/components/legend.directive.js b/ui/src/app/components/legend.directive.js
index fba58e4..196629c 100644
--- a/ui/src/app/components/legend.directive.js
+++ b/ui/src/app/components/legend.directive.js
@@ -44,15 +44,8 @@ function Legend($compile, $templateCache, types) {
         scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
             scope.legendConfig.position === types.position.top.value;
 
-        scope.$on('legendDataUpdated', function (event, apply) {
-            if (apply) {
-                scope.$digest();
-            }
-        });
-
         scope.toggleHideData = function(index) {
             scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
-            scope.$emit('legendDataHiddenChanged', index);
         }
 
         $compile(element.contents())(scope);
diff --git a/ui/src/app/components/socialshare-panel.directive.js b/ui/src/app/components/socialshare-panel.directive.js
new file mode 100644
index 0000000..891d913
--- /dev/null
+++ b/ui/src/app/components/socialshare-panel.directive.js
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import socialsharePanelTemplate from './socialshare-panel.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+export default angular.module('thingsboard.directives.socialsharePanel', [])
+    .directive('tbSocialSharePanel', SocialsharePanel)
+    .name;
+
+/*@ngInject*/
+function SocialsharePanel() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            shareTitle: '@',
+            shareText: '@',
+            shareLink: '@',
+            shareHashTags: '@'
+        },
+        controller: SocialsharePanelController,
+        controllerAs: 'vm',
+        templateUrl: socialsharePanelTemplate
+    };
+}
+
+/*@ngInject*/
+function SocialsharePanelController(utils) {
+
+    let vm = this;
+
+    vm.isShareLinkLocal = function() {
+        if (vm.shareLink && vm.shareLink.length > 0) {
+            return utils.isLocalUrl(vm.shareLink);
+        } else {
+            return true;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/components/socialshare-panel.tpl.html b/ui/src/app/components/socialshare-panel.tpl.html
new file mode 100644
index 0000000..7b9560d
--- /dev/null
+++ b/ui/src/app/components/socialshare-panel.tpl.html
@@ -0,0 +1,62 @@
+<!--
+
+    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" ng-show="!vm.isShareLinkLocal()">
+    <md-button class="md-icon-button md-raised md-primary"
+               socialshare
+               socialshare-provider="facebook"
+               socialshare-title="{{ vm.shareTitle }}"
+               socialshare-text="{{ vm.shareText }}"
+               socialshare-url="{{ vm.shareLink }}">
+        <ng-md-icon icon="facebook" aria-label="Facebook"></ng-md-icon>
+        <md-tooltip md-direction="top">
+            {{ 'action.share-via' | translate:{provider:'Facebook'} }}
+        </md-tooltip>
+    </md-button>
+    <md-button class="md-icon-button md-raised md-primary"
+               socialshare
+               socialshare-provider="twitter"
+               socialshare-text="{{ vm.shareTitle }}"
+               socialshare-hashtags="{{ vm.shareHashTags }}"
+               socialshare-url="{{ vm.shareLink }}">
+        <ng-md-icon icon="twitter" aria-label="Twitter"></ng-md-icon>
+        <md-tooltip md-direction="top">
+            {{ 'action.share-via' | translate:{provider:'Twitter'} }}
+        </md-tooltip>
+    </md-button>
+    <md-button class="md-icon-button md-raised md-primary"
+               socialshare
+               socialshare-provider="linkedin"
+               socialshare-text="{{ vm.shareTitle }}"
+               socialshare-url="{{ vm.shareLink }}">
+        <ng-md-icon icon="linkedin" aria-label="Linkedin"></ng-md-icon>
+        <md-tooltip md-direction="top">
+            {{ 'action.share-via' | translate:{provider:'Linkedin'} }}
+        </md-tooltip>
+    </md-button>
+    <md-button class="md-icon-button md-raised md-primary"
+               socialshare
+               socialshare-provider="reddit"
+               socialshare-text="{{ vm.shareTitle }}"
+               socialshare-url="{{ vm.shareLink }}">
+        <md-icon md-svg-icon="mdi:reddit" aria-label="Reddit"></md-icon>
+        <md-tooltip md-direction="top">
+            {{ 'action.share-via' | translate:{provider:'Reddit'} }}
+        </md-tooltip>
+    </md-button>
+</div>
\ No newline at end of file
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index bf500b1..42299c6 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -15,12 +15,13 @@
  */
 import $ from 'jquery';
 import 'javascript-detect-element-resize/detect-element-resize';
+import Subscription from '../api/subscription';
 
 /* eslint-disable angular/angularelement */
 
 /*@ngInject*/
 export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, tbRaf, types, utils, timeService,
-                                         datasourceService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
+                                         datasourceService, entityService, deviceService, visibleRect, isEdit, stDiff, dashboardTimewindow,
                                          dashboardTimewindowApi, widget, aliasesInfo, widgetType) {
 
     var vm = this;
@@ -34,19 +35,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
     $scope.rpcErrorText = null;
     $scope.rpcEnabled = false;
     $scope.executingRpcRequest = false;
-    $scope.executingPromises = [];
 
     var gridsterItemInited = false;
 
-    var datasourceListeners = [];
-    var targetDeviceAliasId = null;
-    var targetDeviceId = null;
-    var originalTimewindow = null;
-    var subscriptionTimewindow = null;
     var cafs = {};
 
-    var varsRegex = /\$\{([^\}]*)\}/g;
-
     /*
      *   data = array of datasourceData
      *   datasourceData = {
@@ -54,8 +47,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
      *   			dataKey,     { name, config }
      *   			data = array of [time, value]
      *   }
-     *
-     *
      */
 
     var widgetContext = {
@@ -71,22 +62,71 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         settings: widget.config.settings,
         units: widget.config.units || '',
         decimals: angular.isDefined(widget.config.decimals) ? widget.config.decimals : 2,
-        datasources: angular.copy(widget.config.datasources),
-        data: [],
-        hiddenData: [],
-        timeWindow: {
-            stDiff: stDiff
-        },
+        subscriptions: {},
+        defaultSubscription: null,
         timewindowFunctions: {
-            onUpdateTimewindow: onUpdateTimewindow,
-            onResetTimewindow: onResetTimewindow
+            onUpdateTimewindow: function(startTimeMs, endTimeMs) {
+                if (widgetContext.defaultSubscription) {
+                    widgetContext.defaultSubscription.onUpdateTimewindow(startTimeMs, endTimeMs);
+                }
+            },
+            onResetTimewindow: function() {
+                if (widgetContext.defaultSubscription) {
+                    widgetContext.defaultSubscription.onResetTimewindow();
+                }
+            }
+        },
+        subscriptionApi: {
+            createSubscription: function(options, subscribe) {
+                return createSubscription(options, subscribe);
+            },
+
+
+    //      type: "timeseries" or "latest" or "rpc"
+    /*      subscriptionInfo = [
+            {
+                entityType: ""
+                entityId:   ""
+                entityName: ""
+                timeseries: [{ name: "", label: "" }, ..]
+                attributes: [{ name: "", label: "" }, ..]
+            }
+    ..
+    ]*/
+
+    //      options = {
+    //        timeWindowConfig,
+    //        useDashboardTimewindow,
+    //        legendConfig,
+    //        decimals,
+    //        units,
+    //        callbacks [ onDataUpdated(subscription, apply) ]
+    //      }
+    //
+
+            createSubscriptionFromInfo: function (type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
+                return createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe);
+            },
+            removeSubscription: function(id) {
+                var subscription = widgetContext.subscriptions[id];
+                if (subscription) {
+                    subscription.destroy();
+                    delete widgetContext.subscriptions[id];
+                }
+            }
         },
         controlApi: {
             sendOneWayCommand: function(method, params, timeout) {
-                return sendCommand(true, method, params, timeout);
+                if (widgetContext.defaultSubscription) {
+                    return widgetContext.defaultSubscription.sendOneWayCommand(method, params, timeout);
+                }
+                return null;
             },
             sendTwoWayCommand: function(method, params, timeout) {
-                return sendCommand(false, method, params, timeout);
+                if (widgetContext.defaultSubscription) {
+                    return widgetContext.defaultSubscription.sendTwoWayCommand(method, params, timeout);
+                }
+                return null;
             }
         },
         utils: {
@@ -94,7 +134,27 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
     };
 
+    var subscriptionContext = {
+        $scope: $scope,
+        $q: $q,
+        $filter: $filter,
+        $timeout: $timeout,
+        tbRaf: tbRaf,
+        timeService: timeService,
+        deviceService: deviceService,
+        datasourceService: datasourceService,
+        utils: utils,
+        widgetUtils: widgetContext.utils,
+        dashboardTimewindowApi: dashboardTimewindowApi,
+        types: types,
+        stDiff: stDiff,
+        aliasesInfo: aliasesInfo
+    };
+
     var widgetTypeInstance;
+
+    vm.useCustomDatasources = false;
+
     try {
         widgetTypeInstance = new widgetType(widgetContext);
     } catch (e) {
@@ -119,19 +179,14 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
     if (!widgetTypeInstance.onDestroy) {
         widgetTypeInstance.onDestroy = function() {};
     }
-
-    //var bounds = {top: 0, left: 0, bottom: 0, right: 0};
-    //TODO: widgets visibility
-    /*var visible = false;*/
-
-    $scope.clearRpcError = function() {
-        $scope.rpcRejection = null;
-        $scope.rpcErrorText = null;
+    if (widgetTypeInstance.useCustomDatasources) {
+        vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
     }
 
-    vm.gridsterItemInitialized = gridsterItemInitialized;
-
     //TODO: widgets visibility
+
+    //var bounds = {top: 0, left: 0, bottom: 0, right: 0};
+    /*var visible = false;*/
     /*vm.visibleRectChanged = visibleRectChanged;
 
     function visibleRectChanged(newVisibleRect) {
@@ -139,23 +194,226 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         updateVisibility();
     }*/
 
+    $scope.clearRpcError = function() {
+        if (widgetContext.defaultSubscription) {
+            widgetContext.defaultSubscription.clearRpcError();
+        }
+    }
+
+    vm.gridsterItemInitialized = gridsterItemInitialized;
+
     initialize();
 
-    function handleWidgetException(e) {
-        $log.error(e);
-        $scope.widgetErrorData = utils.processWidgetException(e);
+
+    /*
+            options = {
+                 type,
+                 targetDeviceAliasIds,  // RPC
+                 targetDeviceIds,       // RPC
+                 datasources,
+                 timeWindowConfig,
+                 useDashboardTimewindow,
+                 legendConfig,
+                 decimals,
+                 units,
+                 callbacks
+            }
+     */
+
+    function createSubscriptionFromInfo(type, subscriptionsInfo, options, useDefaultComponents, subscribe) {
+        var deferred = $q.defer();
+        options.type = type;
+
+        if (useDefaultComponents) {
+            defaultComponentsOptions(options);
+        } else {
+            if (!options.timeWindowConfig) {
+                options.useDashboardTimewindow = true;
+            }
+        }
+
+        entityService.createDatasoucesFromSubscriptionsInfo(subscriptionsInfo).then(
+            function (datasources) {
+                options.datasources = datasources;
+                var subscription = createSubscription(options, subscribe);
+                if (useDefaultComponents) {
+                    defaultSubscriptionOptions(subscription, options);
+                }
+                deferred.resolve(subscription);
+            }
+        );
+        return deferred.promise;
+    }
+
+    function createSubscription(options, subscribe) {
+        options.dashboardTimewindow = dashboardTimewindow;
+        var subscription =
+            new Subscription(subscriptionContext, options);
+        widgetContext.subscriptions[subscription.id] = subscription;
+        if (subscribe) {
+            subscription.subscribe();
+        }
+        return subscription;
+    }
+
+    function defaultComponentsOptions(options) {
+        options.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
+            ? widget.config.useDashboardTimewindow : true;
+
+        options.timeWindowConfig = options.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;
+        options.legendConfig = null;
+
+        if ($scope.displayLegend) {
+            options.legendConfig = $scope.legendConfig;
+        }
+        options.decimals = widgetContext.decimals;
+        options.units = widgetContext.units;
+
+        options.callbacks = {
+            onDataUpdated: function() {
+                widgetTypeInstance.onDataUpdated();
+            },
+            onDataUpdateError: function(subscription, e) {
+                handleWidgetException(e);
+            },
+            dataLoading: function(subscription) {
+                if ($scope.loadingData !== subscription.loadingData) {
+                    $scope.loadingData = subscription.loadingData;
+                }
+            },
+            legendDataUpdated: function(subscription, apply) {
+                if (apply) {
+                    $scope.$digest();
+                }
+            },
+            timeWindowUpdated: function(subscription, timeWindowConfig) {
+                widget.config.timewindow = timeWindowConfig;
+                $scope.$apply();
+            }
+        }
+    }
+
+    function defaultSubscriptionOptions(subscription, options) {
+        if (!options.useDashboardTimewindow) {
+            $scope.$watch(function () {
+                return widget.config.timewindow;
+            }, function (newTimewindow, prevTimewindow) {
+                if (!angular.equals(newTimewindow, prevTimewindow)) {
+                    subscription.updateTimewindowConfig(widget.config.timewindow);
+                }
+            });
+        }
+        if ($scope.displayLegend) {
+            $scope.legendData = subscription.legendData;
+        }
     }
 
-    function notifyDataLoaded() {
-        if ($scope.loadingData === true) {
+    function createDefaultSubscription() {
+        var subscription;
+        var options;
+        if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
+            options = {
+                type: widget.type,
+                datasources: angular.copy(widget.config.datasources)
+            };
+            defaultComponentsOptions(options);
+
+            subscription = createSubscription(options);
+
+            defaultSubscriptionOptions(subscription, options);
+
+            // backward compatibility
+
+            widgetContext.datasources = subscription.datasources;
+            widgetContext.data = subscription.data;
+            widgetContext.hiddenData = subscription.hiddenData;
+            widgetContext.timeWindow = subscription.timeWindow;
+
+        } else if (widget.type === types.widgetType.rpc.value) {
+            $scope.loadingData = false;
+            options = {
+                type: widget.type,
+                targetDeviceAliasIds: widget.config.targetDeviceAliasIds
+            }
+            options.callbacks = {
+                rpcStateChanged: function(subscription) {
+                    $scope.rpcEnabled = subscription.rpcEnabled;
+                    $scope.executingRpcRequest = subscription.executingRpcRequest;
+                },
+                onRpcSuccess: function(subscription) {
+                    $scope.executingRpcRequest = subscription.executingRpcRequest;
+                    $scope.rpcErrorText = subscription.rpcErrorText;
+                    $scope.rpcRejection = subscription.rpcRejection;
+                },
+                onRpcFailed: function(subscription) {
+                    $scope.executingRpcRequest = subscription.executingRpcRequest;
+                    $scope.rpcErrorText = subscription.rpcErrorText;
+                    $scope.rpcRejection = subscription.rpcRejection;
+                },
+                onRpcErrorCleared: function() {
+                    $scope.rpcErrorText = null;
+                    $scope.rpcRejection = null;
+                }
+            }
+            subscription = createSubscription(options);
+        } else if (widget.type === types.widgetType.static.value) {
             $scope.loadingData = false;
         }
+        if (subscription) {
+            widgetContext.defaultSubscription = subscription;
+        }
     }
 
-    function notifyDataLoading() {
-        if ($scope.loadingData === false) {
-            $scope.loadingData = true;
+
+    function initialize() {
+
+        if (!vm.useCustomDatasources) {
+            createDefaultSubscription();
+        } else {
+            $scope.loadingData = false;
         }
+
+        $scope.$on('toggleDashboardEditMode', function (event, isEdit) {
+            onEditModeChanged(isEdit);
+        });
+
+        addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
+
+        $scope.$watch(function () {
+            return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;
+        }, function () {
+            //updateBounds();
+            $scope.$emit("widgetPositionChanged", widget);
+        });
+
+        $scope.$on('gridster-item-resized', function (event, item) {
+            if (!widgetContext.isMobile) {
+                widget.sizeX = item.sizeX;
+                widget.sizeY = item.sizeY;
+            }
+        });
+
+        $scope.$on('mobileModeChanged', function (event, newIsMobile) {
+            onMobileModeChanged(newIsMobile);
+        });
+
+        $scope.$on('entityAliasListChanged', function (event, aliasesInfo) {
+            subscriptionContext.aliasesInfo = aliasesInfo;
+            for (var id in widgetContext.subscriptions) {
+                var subscription = widgetContext.subscriptions[id];
+                subscription.onAliasesChanged();
+            }
+        });
+
+        $scope.$on("$destroy", function () {
+            removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
+            onDestroy();
+        });
+    }
+
+    function handleWidgetException(e) {
+        $log.error(e);
+        $scope.widgetErrorData = utils.processWidgetException(e);
     }
 
     function onInit() {
@@ -166,42 +424,12 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
             } catch (e) {
                 handleWidgetException(e);
             }
-            if (widgetContext.dataUpdatePending) {
-                widgetContext.dataUpdatePending = false;
-                onDataUpdated();
+            if (!vm.useCustomDatasources && widgetContext.defaultSubscription) {
+                widgetContext.defaultSubscription.subscribe();
             }
         }
     }
 
-    function updateTimewindow() {
-        widgetContext.timeWindow.interval = subscriptionTimewindow.aggregation.interval || 1000;
-        if (subscriptionTimewindow.realtimeWindowMs) {
-            widgetContext.timeWindow.maxTime = (new Date).getTime() + widgetContext.timeWindow.stDiff;
-            widgetContext.timeWindow.minTime = widgetContext.timeWindow.maxTime - subscriptionTimewindow.realtimeWindowMs;
-        } else if (subscriptionTimewindow.fixedWindow) {
-            widgetContext.timeWindow.maxTime = subscriptionTimewindow.fixedWindow.endTimeMs;
-            widgetContext.timeWindow.minTime = subscriptionTimewindow.fixedWindow.startTimeMs;
-        }
-    }
-
-    function onDataUpdated() {
-        if (widgetContext.inited) {
-            if (cafs['dataUpdate']) {
-                cafs['dataUpdate']();
-                cafs['dataUpdate'] = null;
-            }
-            cafs['dataUpdate'] = tbRaf(function() {
-                    try {
-                        widgetTypeInstance.onDataUpdated();
-                    } catch (e) {
-                        handleWidgetException(e);
-                    }
-                });
-        } else {
-            widgetContext.dataUpdatePending = true;
-        }
-    }
-
     function checkSize() {
         var width = widgetContext.$containerParent.width();
         var height = widgetContext.$containerParent.height();
@@ -289,11 +517,35 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
     }
 
+    function isNumeric(val) {
+        return (val - parseFloat( val ) + 1) >= 0;
+    }
+
+    function formatValue(value, dec, units) {
+        if (angular.isDefined(value) &&
+            value !== null && isNumeric(value)) {
+            var formatted = value;
+            if (angular.isDefined(dec)) {
+                formatted = formatted.toFixed(dec);
+            }
+            formatted = (formatted * 1).toString();
+            if (angular.isDefined(units) && units.length > 0) {
+                formatted += ' ' + units;
+            }
+            return formatted;
+        } else {
+            return '';
+        }
+    }
+
     function onDestroy() {
-        unsubscribe();
+        for (var id in widgetContext.subscriptions) {
+            var subscription = widgetContext.subscriptions[id];
+            subscription.destroy();
+        }
+        widgetContext.subscriptions = [];
         if (widgetContext.inited) {
             widgetContext.inited = false;
-            widgetContext.dataUpdatePending = false;
             for (var cafId in cafs) {
                 if (cafs[cafId]) {
                     cafs[cafId]();
@@ -308,244 +560,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
     }
 
-    function onRestart() {
-        onDestroy();
-        onInit();
-    }
-
-/*    scope.legendData = {
-        keys: [],
-        data: []
-
-        key: {
-             label: '',
-             color: ''
-             dataIndex: 0
-        }
-        data: {
-             min: null,
-             max: null,
-             avg: null,
-             total: null
-        }
-    };*/
-
-
-    function initialize() {
-
-        $scope.caulculateLegendData = $scope.displayLegend &&
-            widget.type === types.widgetType.timeseries.value &&
-            ($scope.legendConfig.showMin === true ||
-             $scope.legendConfig.showMax === true ||
-             $scope.legendConfig.showAvg === true ||
-             $scope.legendConfig.showTotal === true);
-
-        if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
             var dataIndex = 0;
-            for (var i = 0; i < widgetContext.datasources.length; i++) {
-                var datasource = widgetContext.datasources[i];
-                for (var a = 0; a < datasource.dataKeys.length; a++) {
-                    var dataKey = datasource.dataKeys[a];
-                    dataKey.pattern = angular.copy(dataKey.label);
-                    var datasourceData = {
-                        datasource: datasource,
-                        dataKey: dataKey,
-                        data: []
-                    };
-                    widgetContext.data.push(datasourceData);
-                    widgetContext.hiddenData.push({data: []});
-                    if ($scope.displayLegend) {
-                        var legendKey = {
-                            dataKey: dataKey,
-                            dataIndex: dataIndex++
-                        };
-                        $scope.legendData.keys.push(legendKey);
-                        var legendKeyData = {
-                            min: null,
-                            max: null,
-                            avg: null,
-                            total: null,
-                            hidden: false
-                        };
-                        $scope.legendData.data.push(legendKeyData);
-                    }
-                }
-            }
-            if ($scope.displayLegend) {
-                $scope.legendData.keys = $filter('orderBy')($scope.legendData.keys, '+label');
-                $scope.$on('legendDataHiddenChanged', function (event, index) {
-                    event.stopPropagation();
-                    var hidden = $scope.legendData.data[index].hidden;
-                    if (hidden) {
-                        widgetContext.hiddenData[index].data = widgetContext.data[index].data;
-                        widgetContext.data[index].data = [];
-                    } else {
-                        widgetContext.data[index].data = widgetContext.hiddenData[index].data;
-                        widgetContext.hiddenData[index].data = [];
-                    }
-                    onDataUpdated();
-                });
-            }
-        } else if (widget.type === types.widgetType.rpc.value) {
-            if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
-                targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
-                if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
-                    targetDeviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
-                }
-            }
-            if (targetDeviceId) {
-                $scope.rpcEnabled = true;
-            } else {
-                $scope.rpcEnabled = $scope.widgetEditMode ? true : false;
-            }
-        }
-
-        $scope.$on('toggleDashboardEditMode', function (event, isEdit) {
-            onEditModeChanged(isEdit);
-        });
-
-        addResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
-
-        $scope.$watch(function () {
-            return widget.row + ',' + widget.col + ',' + widget.config.mobileOrder;
-        }, function () {
-            //updateBounds();
-            $scope.$emit("widgetPositionChanged", widget);
-        });
-
-        $scope.$on('gridster-item-resized', function (event, item) {
-            if (!widgetContext.isMobile) {
-                widget.sizeX = item.sizeX;
-                widget.sizeY = item.sizeY;
-            }
-        });
-
-        $scope.$on('mobileModeChanged', function (event, newIsMobile) {
-            onMobileModeChanged(newIsMobile);
-        });
-
-        $scope.$on('deviceAliasListChanged', function (event, newAliasesInfo) {
-            aliasesInfo = newAliasesInfo;
-            if (widget.type === types.widgetType.rpc.value) {
-                if (targetDeviceAliasId) {
-                    var deviceId = null;
-                    if (aliasesInfo.deviceAliases[targetDeviceAliasId]) {
-                        deviceId = aliasesInfo.deviceAliases[targetDeviceAliasId].deviceId;
-                    }
-                    if (!angular.equals(deviceId, targetDeviceId)) {
-                        targetDeviceId = deviceId;
-                        if (targetDeviceId) {
-                            $scope.rpcEnabled = true;
-                        } else {
-                            $scope.rpcEnabled = $scope.widgetEditMode ? true : false;
-                        }
-                        onRestart();
-                    }
-                }
-            } else {
-                checkSubscriptions();
-            }
-        });
-
-        $scope.$on("$destroy", function () {
-            removeResizeListener(widgetContext.$containerParent[0], onResize); // eslint-disable-line no-undef
-            onDestroy();
-        });
-
-        if (widget.type === types.widgetType.timeseries.value) {
-            widgetContext.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
-                    ? widget.config.useDashboardTimewindow : true;
-            if (widgetContext.useDashboardTimewindow) {
-                $scope.$on('dashboardTimewindowChanged', function (event, newDashboardTimewindow) {
-                    if (!angular.equals(dashboardTimewindow, newDashboardTimewindow)) {
-                        dashboardTimewindow = newDashboardTimewindow;
-                        unsubscribe();
-                        subscribe();
-                    }
-                });
-            } else {
-                $scope.$watch(function () {
-                    return widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow;
-                }, function (newTimewindow, prevTimewindow) {
-                    if (!angular.equals(newTimewindow, prevTimewindow)) {
-                        unsubscribe();
-                        subscribe();
-                    }
-                });
-            }
-        }
-        subscribe();
-    }
-
-    function sendCommand(oneWayElseTwoWay, method, params, timeout) {
-        if (!$scope.rpcEnabled) {
-            return $q.reject();
-        }
-
-        if ($scope.rpcRejection && $scope.rpcRejection.status !== 408) {
-            $scope.rpcRejection = null;
-            $scope.rpcErrorText = null;
-        }
-
-        var requestBody = {
-            method: method,
-            params: params
-        };
-
-        if (timeout && timeout > 0) {
-            requestBody.timeout = timeout;
-        }
-
-        var deferred = $q.defer();
-        $scope.executingRpcRequest = true;
-        if ($scope.widgetEditMode) {
-            $timeout(function() {
-                $scope.executingRpcRequest = false;
-                if (oneWayElseTwoWay) {
-                    deferred.resolve();
-                } else {
-                    deferred.resolve(requestBody);
-                }
-            }, 500);
-        } else {
-            $scope.executingPromises.push(deferred.promise);
-            var targetSendFunction = oneWayElseTwoWay ? deviceService.sendOneWayRpcCommand : deviceService.sendTwoWayRpcCommand;
-            targetSendFunction(targetDeviceId, requestBody).then(
-                function success(responseBody) {
-                    $scope.rpcRejection = null;
-                    $scope.rpcErrorText = null;
-                    var index = $scope.executingPromises.indexOf(deferred.promise);
-                    if (index >= 0) {
-                        $scope.executingPromises.splice( index, 1 );
-                    }
-                    $scope.executingRpcRequest = $scope.executingPromises.length > 0;
-                    deferred.resolve(responseBody);
-                },
-                function fail(rejection) {
-                    var index = $scope.executingPromises.indexOf(deferred.promise);
-                    if (index >= 0) {
-                        $scope.executingPromises.splice( index, 1 );
-                    }
-                    $scope.executingRpcRequest = $scope.executingPromises.length > 0;
-                    if (!$scope.executingRpcRequest || rejection.status === 408) {
-                        $scope.rpcRejection = rejection;
-                        if (rejection.status === 408) {
-                            $scope.rpcErrorText = 'Device is offline.';
-                        } else {
-                            $scope.rpcErrorText =  'Error : ' + rejection.status + ' - ' + rejection.statusText;
-                            if (rejection.data && rejection.data.length > 0) {
-                                $scope.rpcErrorText += '</br>';
-                                $scope.rpcErrorText += rejection.data;
-                            }
-                        }
-                    }
-                    deferred.reject(rejection);
-                }
-            );
-        }
-        return deferred.promise;
-    }
-
     //TODO: widgets visibility
     /*function updateVisibility(forceRedraw) {
         if (visibleRect) {
@@ -584,285 +599,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         onRedraw();
     }*/
 
-    function onResetTimewindow() {
-        if (widgetContext.useDashboardTimewindow) {
-            dashboardTimewindowApi.onResetTimewindow();
-        } else {
-            if (originalTimewindow) {
-                widget.config.timewindow = angular.copy(originalTimewindow);
-                originalTimewindow = null;
-            }
-        }
-    }
-
-    function onUpdateTimewindow(startTimeMs, endTimeMs) {
-        if (widgetContext.useDashboardTimewindow) {
-            dashboardTimewindowApi.onUpdateTimewindow(startTimeMs, endTimeMs);
-        } else {
-            if (!originalTimewindow) {
-                originalTimewindow = angular.copy(widget.config.timewindow);
-            }
-            widget.config.timewindow = timeService.toHistoryTimewindow(widget.config.timewindow, startTimeMs, endTimeMs);
-        }
-    }
-
-    function dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
-        notifyDataLoaded();
-        var update = true;
-        var currentData;
-        if ($scope.displayLegend && $scope.legendData.data[datasourceIndex + dataKeyIndex].hidden) {
-            currentData = widgetContext.hiddenData[datasourceIndex + dataKeyIndex];
-        } else {
-            currentData = widgetContext.data[datasourceIndex + dataKeyIndex];
-        }
-        if (widget.type === types.widgetType.latest.value) {
-            var prevData = currentData.data;
-            if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) {
-                var prevValue = prevData[0][1];
-                if (prevValue === sourceData.data[0][1]) {
-                    update = false;
-                }
-            }
-        }
-        if (update) {
-            if (subscriptionTimewindow && subscriptionTimewindow.realtimeWindowMs) {
-                updateTimewindow();
-            }
-            currentData.data = sourceData.data;
-            onDataUpdated();
-            if ($scope.caulculateLegendData) {
-                updateLegend(datasourceIndex + dataKeyIndex, sourceData.data, apply);
-            }
-        }
-        if (apply) {
-            $scope.$digest();
-        }
-    }
-
-    function updateLegend(dataIndex, data, apply) {
-        var legendKeyData = $scope.legendData.data[dataIndex];
-        if ($scope.legendConfig.showMin) {
-            legendKeyData.min = formatValue(calculateMin(data), widgetContext.decimals, widgetContext.units);
-        }
-        if ($scope.legendConfig.showMax) {
-            legendKeyData.max = formatValue(calculateMax(data), widgetContext.decimals, widgetContext.units);
-        }
-        if ($scope.legendConfig.showAvg) {
-            legendKeyData.avg = formatValue(calculateAvg(data), widgetContext.decimals, widgetContext.units);
-        }
-        if ($scope.legendConfig.showTotal) {
-            legendKeyData.total = formatValue(calculateTotal(data), widgetContext.decimals, widgetContext.units);
-        }
-        $scope.$broadcast('legendDataUpdated', apply !== false);
-    }
-
-    function isNumeric(val) {
-        return (val - parseFloat( val ) + 1) >= 0;
-    }
-
-    function formatValue(value, dec, units) {
-        if (angular.isDefined(value) &&
-            value !== null && isNumeric(value)) {
-            var formatted = value;
-            if (angular.isDefined(dec)) {
-                formatted = formatted.toFixed(dec);
-            }
-            formatted = (formatted * 1).toString();
-            if (angular.isDefined(units) && units.length > 0) {
-                formatted += ' ' + units;
-            }
-            return formatted;
-        } else {
-            return '';
-        }
-    }
-
-    function calculateMin(data) {
-        if (data.length > 0) {
-            var result = Number(data[0][1]);
-            for (var i=1;i<data.length;i++) {
-                result = Math.min(result, Number(data[i][1]));
-            }
-            return result;
-        } else {
-            return null;
-        }
-    }
-
-    function calculateMax(data) {
-        if (data.length > 0) {
-            var result = Number(data[0][1]);
-            for (var i=1;i<data.length;i++) {
-                result = Math.max(result, Number(data[i][1]));
-            }
-            return result;
-        } else {
-            return null;
-        }
-    }
-
-    function calculateAvg(data) {
-        if (data.length > 0) {
-            return calculateTotal(data)/data.length;
-        } else {
-            return null;
-        }
-    }
-
-    function calculateTotal(data) {
-        if (data.length > 0) {
-            var result = 0;
-            for (var i = 0; i < data.length; i++) {
-                result += Number(data[i][1]);
-            }
-            return result;
-        } else {
-            return null;
-        }
-    }
-
-    function checkSubscriptions() {
-        if (widget.type !== types.widgetType.rpc.value) {
-            var subscriptionsChanged = false;
-            for (var i = 0; i < datasourceListeners.length; i++) {
-                var listener = datasourceListeners[i];
-                var deviceId = null;
-                var aliasName = null;
-                if (listener.datasource.type === types.datasourceType.device) {
-                    if (aliasesInfo.deviceAliases[listener.datasource.deviceAliasId]) {
-                        deviceId = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].deviceId;
-                        aliasName = aliasesInfo.deviceAliases[listener.datasource.deviceAliasId].alias;
-                    }
-                    if (!angular.equals(deviceId, listener.deviceId) ||
-                        !angular.equals(aliasName, listener.datasource.name)) {
-                        subscriptionsChanged = true;
-                        break;
-                    }
-                }
-            }
-            if (subscriptionsChanged) {
-                unsubscribe();
-                subscribe();
-            }
-        }
-    }
-
-    function unsubscribe() {
-        if (widget.type !== types.widgetType.rpc.value) {
-            for (var i = 0; i < datasourceListeners.length; i++) {
-                var listener = datasourceListeners[i];
-                datasourceService.unsubscribeFromDatasource(listener);
-            }
-            datasourceListeners = [];
-        }
-    }
-
-    function updateRealtimeSubscription(_subscriptionTimewindow) {
-        if (_subscriptionTimewindow) {
-            subscriptionTimewindow = _subscriptionTimewindow;
-        } else {
-            subscriptionTimewindow =
-                timeService.createSubscriptionTimewindow(
-                    widgetContext.useDashboardTimewindow ? dashboardTimewindow : widget.config.timewindow,
-                    widgetContext.timeWindow.stDiff);
-        }
-        updateTimewindow();
-        return subscriptionTimewindow;
-    }
-
-    function hasTimewindow() {
-        if (widgetContext.useDashboardTimewindow) {
-            return angular.isDefined(dashboardTimewindow);
-        } else {
-            return angular.isDefined(widget.config.timewindow);
-        }
-    }
-
-    function updateDataKeyLabel(dataKey, deviceName, aliasName) {
-        var pattern = dataKey.pattern;
-        var label = dataKey.pattern;
-        var match = varsRegex.exec(pattern);
-        while (match !== null) {
-            var variable = match[0];
-            var variableName = match[1];
-            if (variableName === 'deviceName') {
-                label = label.split(variable).join(deviceName);
-            } else if (variableName === 'aliasName') {
-                label = label.split(variable).join(aliasName);
-            }
-            match = varsRegex.exec(pattern);
-        }
-        dataKey.label = label;
-    }
-
-    function subscribe() {
-        if (widget.type !== types.widgetType.rpc.value && widget.type !== types.widgetType.static.value) {
-            notifyDataLoading();
-            if (widget.type === types.widgetType.timeseries.value &&
-                hasTimewindow()) {
-                updateRealtimeSubscription();
-                if (subscriptionTimewindow.fixedWindow) {
-                    onDataUpdated();
-                }
-            }
-            var index = 0;
-            for (var i = 0; i < widgetContext.datasources.length; i++) {
-                var datasource = widgetContext.datasources[i];
-                if (angular.isFunction(datasource))
-                    continue;
-                var deviceId = null;
-                if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
-                    if (aliasesInfo.deviceAliases[datasource.deviceAliasId]) {
-                        deviceId = aliasesInfo.deviceAliases[datasource.deviceAliasId].deviceId;
-                        datasource.name = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
-                        var aliasName = aliasesInfo.deviceAliases[datasource.deviceAliasId].alias;
-                        var deviceName = '';
-                        var devicesInfo = aliasesInfo.deviceAliasesInfo[datasource.deviceAliasId];
-                        for (var d=0;d<devicesInfo.length;d++) {
-                            if (devicesInfo[d].id === deviceId) {
-                                deviceName = devicesInfo[d].name;
-                                break;
-                            }
-                        }
-                        for (var dk = 0; dk < datasource.dataKeys.length; dk++) {
-                            updateDataKeyLabel(datasource.dataKeys[dk], deviceName, aliasName);
-                        }
-                    }
-                } else {
-                    datasource.name = types.datasourceType.function;
-                }
-                var listener = {
-                    widget: widget,
-                    subscriptionTimewindow: subscriptionTimewindow,
-                    datasource: datasource,
-                    deviceId: deviceId,
-                    dataUpdated: function (data, datasourceIndex, dataKeyIndex, apply) {
-                        dataUpdated(data, datasourceIndex, dataKeyIndex, apply);
-                    },
-                    updateRealtimeSubscription: function() {
-                        this.subscriptionTimewindow = updateRealtimeSubscription();
-                        return this.subscriptionTimewindow;
-                    },
-                    setRealtimeSubscription: function(subscriptionTimewindow) {
-                        updateRealtimeSubscription(angular.copy(subscriptionTimewindow));
-                    },
-                    datasourceIndex: index
-                };
-
-                for (var a = 0; a < datasource.dataKeys.length; a++) {
-                    widgetContext.data[index + a].data = [];
-                }
-
-                index += datasource.dataKeys.length;
-
-                datasourceListeners.push(listener);
-                datasourceService.subscribeToDatasource(listener);
-            }
-        } else {
-            notifyDataLoaded();
-        }
-    }
-
 }
 
 /* eslint-enable angular/angularelement */
\ No newline at end of file
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index 61a5d14..6c7c472 100644
--- a/ui/src/app/components/widget-config.directive.js
+++ b/ui/src/app/components/widget-config.directive.js
@@ -16,7 +16,7 @@
 import jsonSchemaDefaults from 'json-schema-defaults';
 import thingsboardTypes from '../common/types.constant';
 import thingsboardUtils from '../common/utils.service';
-import thingsboardDeviceAliasSelect from './device-alias-select.directive';
+import thingsboardEntityAliasSelect from './entity-alias-select.directive';
 import thingsboardDatasource from './datasource.directive';
 import thingsboardTimewindow from './timewindow.directive';
 import thingsboardLegendConfig from './legend-config.directive';
@@ -34,7 +34,7 @@ import widgetConfigTemplate from './widget-config.tpl.html';
 export default angular.module('thingsboard.directives.widgetConfig', [thingsboardTypes,
     thingsboardUtils,
     thingsboardJsonForm,
-    thingsboardDeviceAliasSelect,
+    thingsboardEntityAliasSelect,
     thingsboardDatasource,
     thingsboardTimewindow,
     thingsboardLegendConfig,
@@ -76,6 +76,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
             scope.forceExpandDatasources = false;
         }
 
+        if (angular.isUndefined(scope.isDataEnabled)) {
+            scope.isDataEnabled = true;
+        }
+
         scope.currentSettingsSchema = {};
         scope.currentSettings = angular.copy(scope.emptySettingsSchema);
 
@@ -108,7 +112,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
                 scope.showLegend = angular.isDefined(ngModelCtrl.$viewValue.showLegend) ?
                     ngModelCtrl.$viewValue.showLegend : scope.widgetType === types.widgetType.timeseries.value;
                 scope.legendConfig = ngModelCtrl.$viewValue.legendConfig;
-                if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) {
+                if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value
+                    && scope.isDataEnabled) {
                     if (scope.datasources) {
                         scope.datasources.splice(0, scope.datasources.length);
                     } else {
@@ -119,12 +124,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
                             scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
                         }
                     }
-                } else if (scope.widgetType === types.widgetType.rpc.value) {
+                } else if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
                     if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
                         var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
-                        if (scope.deviceAliases[aliasId]) {
-                            scope.targetDeviceAlias.value = {id: aliasId, alias: scope.deviceAliases[aliasId].alias,
-                                deviceId: scope.deviceAliases[aliasId].deviceId};
+                        if (scope.entityAliases[aliasId]) {
+                            scope.targetDeviceAlias.value = {id: aliasId, alias: scope.entityAliases[aliasId].alias,
+                                entityType: scope.entityAliases[aliasId].entityType, entityId: scope.entityAliases[aliasId].entityId};
                         } else {
                             scope.targetDeviceAlias.value = null;
                         }
@@ -159,10 +164,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
                 var valid;
-                if (scope.widgetType === types.widgetType.rpc.value) {
+                if (scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
                     valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
                     ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
-                } else if (scope.widgetType !== types.widgetType.static.value) {
+                } else if (scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
                     valid = value && value.datasources && value.datasources.length > 0;
                     ngModelCtrl.$setValidity('datasources', valid);
                 }
@@ -228,7 +233,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
 
         scope.$watch('datasources', function () {
             if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value
-                && scope.widgetType !== types.widgetType.static.value) {
+                && scope.widgetType !== types.widgetType.static.value && scope.isDataEnabled) {
                 var value = ngModelCtrl.$viewValue;
                 if (value.datasources) {
                     value.datasources.splice(0, value.datasources.length);
@@ -246,7 +251,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
         }, true);
 
         scope.$watch('targetDeviceAlias.value', function () {
-            if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value) {
+            if (ngModelCtrl.$viewValue && scope.widgetType === types.widgetType.rpc.value && scope.isDataEnabled) {
                 var value = ngModelCtrl.$viewValue;
                 if (scope.targetDeviceAlias.value) {
                     value.targetDeviceAliasIds = [scope.targetDeviceAlias.value.id];
@@ -264,7 +269,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
                 newDatasource = angular.copy(utils.getDefaultDatasource(scope.datakeySettingsSchema.schema));
                 newDatasource.dataKeys = [scope.generateDataKey('Sin', types.dataKeyType.function)];
             } else {
-                newDatasource = { type: types.datasourceType.device,
+                newDatasource = { type: types.datasourceType.entity,
                     dataKeys: []
                 };
             }
@@ -359,13 +364,14 @@ function WidgetConfig($compile, $templateCache, $rootScope, $timeout, types, uti
         require: "^ngModel",
         scope: {
             forceExpandDatasources: '=?',
+            isDataEnabled: '=?',
             widgetType: '=',
             widgetSettingsSchema: '=',
             datakeySettingsSchema: '=',
-            deviceAliases: '=',
+            entityAliases: '=',
             functionsOnly: '=',
-            fetchDeviceKeys: '&',
-            onCreateDeviceAlias: '&',
+            fetchEntityKeys: '&',
+            onCreateEntityAlias: '&',
             theForm: '='
         },
         link: linker
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index 66b440f..808e07b 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -31,7 +31,7 @@
                 </section>
             </div>
             <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
-                         ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value">
+                         ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value && isDataEnabled">
                 <v-pane id="datasources-pane" expanded="true">
                     <v-pane-header>
                         {{ 'widget-config.datasources' | translate }}
@@ -60,12 +60,12 @@
                                          style="padding: 0 0 0 10px; margin: 5px;">
                                         <tb-datasource flex ng-model="datasource.value"
                                                        widget-type="widgetType"
-                                                       device-aliases="deviceAliases"
+                                                       entity-aliases="entityAliases"
                                                        functions-only="functionsOnly"
                                                        datakey-settings-schema="datakeySettingsSchema"
                                                        generate-data-key="generateDataKey(chip,type)"
-                                                       fetch-device-keys="fetchDeviceKeys({deviceAliasId: deviceAliasId, query: query, type: type})"
-                                                       on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})"></tb-datasource>
+                                                       fetch-entity-keys="fetchEntityKeys({entityAliasId: entityAliasId, query: query, type: type})"
+                                                       on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias})"></tb-datasource>
                                         <md-button ng-disabled="loading" class="md-icon-button md-primary"
                                                    style="min-width: 40px;"
                                                    ng-click="removeDatasource($event, datasource)"
@@ -96,18 +96,19 @@
                 </v-pane>
             </v-accordion>
             <v-accordion id="target-devices-accordion" control="targetDevicesAccordion" class="vAccordion--default"
-                         ng-show="widgetType === types.widgetType.rpc.value">
+                         ng-show="widgetType === types.widgetType.rpc.value && isDataEnabled">
                 <v-pane id="target-devices-pane" expanded="true">
                     <v-pane-header>
                         {{ 'widget-config.target-device' | translate }}
                     </v-pane-header>
                     <v-pane-content style="padding: 0 5px;">
-                        <tb-device-alias-select flex
+                        <tb-entity-alias-select flex
                                                 tb-required="widgetType === types.widgetType.rpc.value && !widgetEditMode"
-                                                device-aliases="deviceAliases"
+                                                entity-aliases="entityAliases"
+                                                allowed-entity-types="[types.entityType.device]"
                                                 ng-model="targetDeviceAlias.value"
-                                                on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
-                        </tb-device-alias-select>
+                                                on-create-entity-alias="onCreateEntityAlias({event: event, alias: alias, allowedEntityTypes: allowedEntityTypes})">
+                        </tb-entity-alias-select>
                     </v-pane-content>
                 </v-pane>
             </v-accordion>
diff --git a/ui/src/app/customer/customer.controller.js b/ui/src/app/customer/customer.controller.js
index 5738660..5175052 100644
--- a/ui/src/app/customer/customer.controller.js
+++ b/ui/src/app/customer/customer.controller.js
@@ -21,7 +21,7 @@ import customerCard from './customer-card.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function CustomerController(customerService, $state, $stateParams, $translate) {
+export default function CustomerController(customerService, $state, $stateParams, $translate, types) {
 
     var customerActionsList = [
         {
@@ -30,14 +30,37 @@ export default function CustomerController(customerService, $state, $stateParams
             },
             name: function() { return $translate.instant('user.users') },
             details: function() { return $translate.instant('customer.manage-customer-users') },
-            icon: "account_circle"
+            icon: "account_circle",
+            isEnabled: function(customer) {
+                return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
+            }
+        },
+        {
+            onAction: function ($event, item) {
+                openCustomerAssets($event, item);
+            },
+            name: function() { return $translate.instant('asset.assets') },
+            details: function(customer) {
+                if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) {
+                    return $translate.instant('customer.manage-public-assets')
+                } else {
+                    return $translate.instant('customer.manage-customer-assets')
+                }
+            },
+            icon: "domain"
         },
         {
             onAction: function ($event, item) {
                 openCustomerDevices($event, item);
             },
             name: function() { return $translate.instant('device.devices') },
-            details: function() { return $translate.instant('customer.manage-customer-devices') },
+            details: function(customer) {
+                if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) {
+                    return $translate.instant('customer.manage-public-devices')
+                } else {
+                    return $translate.instant('customer.manage-customer-devices')
+                }
+            },
             icon: "devices_other"
         },
         {
@@ -45,7 +68,13 @@ export default function CustomerController(customerService, $state, $stateParams
                 openCustomerDashboards($event, item);
             },
             name: function() { return $translate.instant('dashboard.dashboards') },
-            details: function() { return $translate.instant('customer.manage-customer-dashboards') },
+            details: function(customer) {
+                if (customer && customer.additionalInfo && customer.additionalInfo.isPublic) {
+                    return $translate.instant('customer.manage-public-dashboards')
+                } else {
+                    return $translate.instant('customer.manage-customer-dashboards')
+                }
+            },
             icon: "dashboard"
         },
         {
@@ -54,12 +83,17 @@ export default function CustomerController(customerService, $state, $stateParams
             },
             name: function() { return $translate.instant('action.delete') },
             details: function() { return $translate.instant('customer.delete') },
-            icon: "delete"
+            icon: "delete",
+            isEnabled: function(customer) {
+                return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
+            }
         }
     ];
 
     var vm = this;
 
+    vm.types = types;
+
     vm.customerGridConfig = {
 
         refreshParamsFunc: null,
@@ -86,7 +120,19 @@ export default function CustomerController(customerService, $state, $stateParams
 
         addItemText: function() { return $translate.instant('customer.add-customer-text') },
         noItemsText: function() { return $translate.instant('customer.no-customers-text') },
-        itemDetailsText: function() { return $translate.instant('customer.customer-details') }
+        itemDetailsText: function(customer) {
+            if (customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic)) {
+                return $translate.instant('customer.customer-details')
+            } else {
+                return '';
+            }
+        },
+        isSelectionEnabled: function (customer) {
+            return customer && (!customer.additionalInfo || !customer.additionalInfo.isPublic);
+        },
+        isDetailsReadOnly: function (customer) {
+            return customer && customer.additionalInfo && customer.additionalInfo.isPublic;
+        }
     };
 
     if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
@@ -98,6 +144,7 @@ export default function CustomerController(customerService, $state, $stateParams
     }
 
     vm.openCustomerUsers = openCustomerUsers;
+    vm.openCustomerAssets = openCustomerAssets;
     vm.openCustomerDevices = openCustomerDevices;
     vm.openCustomerDashboards = openCustomerDashboards;
 
@@ -148,6 +195,13 @@ export default function CustomerController(customerService, $state, $stateParams
         $state.go('home.customers.users', {customerId: customer.id.id});
     }
 
+    function openCustomerAssets($event, customer) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        $state.go('home.customers.assets', {customerId: customer.id.id});
+    }
+
     function openCustomerDevices($event, customer) {
         if ($event) {
             $event.stopPropagation();
diff --git a/ui/src/app/customer/customer.directive.js b/ui/src/app/customer/customer.directive.js
index f23711a..e48934b 100644
--- a/ui/src/app/customer/customer.directive.js
+++ b/ui/src/app/customer/customer.directive.js
@@ -20,11 +20,29 @@ import customerFieldsetTemplate from './customer-fieldset.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function CustomerDirective($compile, $templateCache) {
+export default function CustomerDirective($compile, $templateCache, $translate, toast) {
     var linker = function (scope, element) {
         var template = $templateCache.get(customerFieldsetTemplate);
         element.html(template);
+
+        scope.isPublic = false;
+
+        scope.onCustomerIdCopied = function() {
+            toast.showSuccess($translate.instant('customer.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
+        scope.$watch('customer', function(newVal) {
+            if (newVal) {
+                if (scope.customer.additionalInfo) {
+                    scope.isPublic = scope.customer.additionalInfo.isPublic;
+                } else {
+                    scope.isPublic = false;
+                }
+            }
+        });
+
         $compile(element.contents())(scope);
+
     }
     return {
         restrict: "E",
@@ -34,6 +52,7 @@ export default function CustomerDirective($compile, $templateCache) {
             isEdit: '=',
             theForm: '=',
             onManageUsers: '&',
+            onManageAssets: '&',
             onManageDevices: '&',
             onManageDashboards: '&',
             onDeleteCustomer: '&'
diff --git a/ui/src/app/customer/customer-card.tpl.html b/ui/src/app/customer/customer-card.tpl.html
index 8c96313..e15ed9d 100644
--- a/ui/src/app/customer/customer-card.tpl.html
+++ b/ui/src/app/customer/customer-card.tpl.html
@@ -15,4 +15,4 @@
     limitations under the License.
 
 -->
-<div class="tb-uppercase">{{ item | contactShort }}</div>
\ No newline at end of file
+<div ng-show="item && (!item.additionalInfo || !item.additionalInfo.isPublic)" class="tb-uppercase">{{ item | contactShort }}</div>
\ No newline at end of file
diff --git a/ui/src/app/customer/customer-fieldset.tpl.html b/ui/src/app/customer/customer-fieldset.tpl.html
index 62ef6b3..3facd0f 100644
--- a/ui/src/app/customer/customer-fieldset.tpl.html
+++ b/ui/src/app/customer/customer-fieldset.tpl.html
@@ -15,13 +15,24 @@
     limitations under the License.
 
 -->
-<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
+<md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.manage-users' | translate }}</md-button>
+<md-button ng-click="onManageAssets({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.manage-assets' | translate }}</md-button>
 <md-button ng-click="onManageDevices({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-devices' | translate }}</md-button>
 <md-button ng-click="onManageDashboards({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.manage-dashboards' | translate }}</md-button>
-<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
+<md-button ng-click="onDeleteCustomer({event: $event})" ng-show="!isEdit && !isPublic" class="md-raised md-primary">{{ 'customer.delete' | translate }}</md-button>
+
+<div layout="row">
+	<md-button ngclipboard data-clipboard-action="copy"
+			   ngclipboard-success="onCustomerIdCopied(e)"
+			   data-clipboard-text="{{customer.id.id}}" ng-show="!isEdit"
+			   class="md-raised">
+		<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+		<span translate>customer.copyId</span>
+	</md-button>
+</div>
 
 <md-content class="md-padding" layout="column">
-	<fieldset ng-disabled="loading || !isEdit">
+	<fieldset ng-show="!isPublic" ng-disabled="loading || !isEdit">
 		<md-input-container class="md-block">
 			<label translate>customer.title</label>
 			<input required name="title" ng-model="customer.title">	
diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html
index 7d706be..9221984 100644
--- a/ui/src/app/customer/customers.tpl.html
+++ b/ui/src/app/customer/customers.tpl.html
@@ -19,11 +19,41 @@
 	<details-buttons tb-help="'customers'" help-container-id="help-container">
 		<div id="help-container"></div>
 	</details-buttons>
-	<tb-customer customer="vm.grid.operatingItem()"
-			is-edit="vm.grid.detailsConfig.isDetailsEditMode"
-			the-form="vm.grid.detailsForm"
-			on-manage-users="vm.openCustomerUsers(event, vm.grid.detailsConfig.currentItem)"
-			on-manage-devices="vm.openCustomerDevices(event, vm.grid.detailsConfig.currentItem)"
-			on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)"
-			on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-customer>
+	<md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+			 id="tabs" md-border-bottom flex class="tb-absolute-fill">
+		<md-tab label="{{ 'customer.details' | translate }}">
+			<tb-customer customer="vm.grid.operatingItem()"
+				is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+				the-form="vm.grid.detailsForm"
+				on-manage-users="vm.openCustomerUsers(event, vm.grid.detailsConfig.currentItem)"
+				on-manage-assets="vm.openCustomerAssets(event, vm.grid.detailsConfig.currentItem)"
+				on-manage-devices="vm.openCustomerDevices(event, vm.grid.detailsConfig.currentItem)"
+				on-manage-dashboards="vm.openCustomerDashboards(event, vm.grid.detailsConfig.currentItem)"
+				on-delete-customer="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-customer>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+			<tb-attribute-table flex
+								entity-id="vm.grid.operatingItem().id.id"
+								entity-type="{{vm.types.entityType.customer}}"
+								entity-name="vm.grid.operatingItem().title"
+								default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+			</tb-attribute-table>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+			<tb-attribute-table flex
+								entity-id="vm.grid.operatingItem().id.id"
+								entity-type="{{vm.types.entityType.customer}}"
+								entity-name="vm.grid.operatingItem().title"
+								default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+								disable-attribute-scope-selection="true">
+			</tb-attribute-table>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'customer.events' | translate }}">
+			<tb-event-table flex entity-type="vm.types.entityType.customer"
+							entity-id="vm.grid.operatingItem().id.id"
+							tenant-id="vm.grid.operatingItem().tenantId.id"
+							default-event-type="{{vm.types.eventType.alarm.value}}">
+			</tb-event-table>
+		</md-tab>
+	</md-tabs>
 </tb-grid>
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index e9ceaad..de3bf95 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -15,12 +15,12 @@
  */
 /* eslint-disable import/no-unresolved, import/default */
 
-import deviceAliasesTemplate from './device-aliases.tpl.html';
+import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function AddWidgetController($scope, widgetService, deviceService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
+export default function AddWidgetController($scope, widgetService, entityService, $mdDialog, $q, $document, types, dashboard, aliasesInfo, widget, widgetInfo) {
 
     var vm = this;
 
@@ -34,8 +34,8 @@ export default function AddWidgetController($scope, widgetService, deviceService
     vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
     vm.add = add;
     vm.cancel = cancel;
-    vm.fetchDeviceKeys = fetchDeviceKeys;
-    vm.createDeviceAlias = createDeviceAlias;
+    vm.fetchEntityKeys = fetchEntityKeys;
+    vm.createEntityAlias = createEntityAlias;
 
     vm.widgetConfig = vm.widget.config;
 
@@ -90,45 +90,46 @@ export default function AddWidgetController($scope, widgetService, deviceService
         }
     }
 
-    function fetchDeviceKeys (deviceAliasId, query, type) {
-        var deviceAlias = vm.aliasesInfo.deviceAliases[deviceAliasId];
-        if (deviceAlias && deviceAlias.deviceId) {
-            return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
+    function fetchEntityKeys (entityAliasId, query, type) {
+        var entityAlias = vm.aliasesInfo.entityAliases[entityAliasId];
+        if (entityAlias && entityAlias.entityId) {
+            return entityService.getEntityKeys(entityAlias.entityType, entityAlias.entityId, query, type);
         } else {
             return $q.when([]);
         }
     }
 
-    function createDeviceAlias (event, alias) {
+    function createEntityAlias (event, alias, allowedEntityTypes) {
 
         var deferred = $q.defer();
-        var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
+        var singleEntityAlias = {id: null, alias: alias, entityType: types.entityType.device, entityFilter: null};
 
         $mdDialog.show({
-            controller: 'DeviceAliasesController',
+            controller: 'EntityAliasesController',
             controllerAs: 'vm',
-            templateUrl: deviceAliasesTemplate,
+            templateUrl: entityAliasesTemplate,
             locals: {
                 config: {
-                    deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+                    entityAliases: angular.copy(vm.dashboard.configuration.entityAliases),
                     widgets: null,
-                    isSingleDeviceAlias: true,
-                    singleDeviceAlias: singleDeviceAlias
+                    isSingleEntityAlias: true,
+                    singleEntityAlias: singleEntityAlias,
+                    allowedEntityTypes: allowedEntityTypes
                 }
             },
             parent: angular.element($document[0].body),
             fullscreen: true,
             skipHide: true,
             targetEvent: event
-        }).then(function (singleDeviceAlias) {
-            vm.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
-                { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
-            deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases).then(
+        }).then(function (singleEntityAlias) {
+            vm.dashboard.configuration.entityAliases[singleEntityAlias.id] =
+                { alias: singleEntityAlias.alias, entityType: singleEntityAlias.entityType, entityFilter: singleEntityAlias.entityFilter };
+            entityService.processEntityAliases(vm.dashboard.configuration.entityAliases).then(
                 function(resolution) {
                     if (!resolution.error) {
                         vm.aliasesInfo = resolution.aliasesInfo;
                     }
-                    deferred.resolve(singleDeviceAlias);
+                    deferred.resolve(singleEntityAlias);
                 }
             );
         }, function () {
diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
index 4e6db50..870e203 100644
--- a/ui/src/app/dashboard/add-widget.tpl.html
+++ b/ui/src/app/dashboard/add-widget.tpl.html
@@ -37,10 +37,10 @@
                                       ng-model="vm.widgetConfig"
                                       widget-settings-schema="vm.settingsSchema"
                                       datakey-settings-schema="vm.dataKeySettingsSchema"
-                                      device-aliases="vm.aliasesInfo.deviceAliases"
+                                      entity-aliases="vm.aliasesInfo.entityAliases"
                                       functions-only="vm.functionsOnly"
-                                      fetch-device-keys="vm.fetchDeviceKeys(deviceAliasId, query, type)"
-                                      on-create-device-alias="vm.createDeviceAlias(event, alias)"
+                                      fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)"
+                                      on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)"
                                       the-form="theForm"></tb-widget-config>
                 </fieldset>
             </div>
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index fbb3e37..79ee6ae 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -15,15 +15,15 @@
  */
 /* eslint-disable import/no-unresolved, import/default */
 
-import deviceAliasesTemplate from './device-aliases.tpl.html';
+import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
 import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
 import addWidgetTemplate from './add-widget.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DashboardController(types, widgetService, userService,
-                                            dashboardService, timeService, deviceService, itembuffer, importExport, hotkeys, $window, $rootScope,
+export default function DashboardController(types, dashboardUtils, widgetService, userService,
+                                            dashboardService, timeService, entityService, itembuffer, importExport, hotkeys, $window, $rootScope,
                                             $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
 
     var vm = this;
@@ -48,6 +48,8 @@ export default function DashboardController(types, widgetService, userService,
 
     vm.isToolbarOpened = false;
 
+    vm.thingsboardVersion = THINGSBOARD_VERSION; //eslint-disable-line
+
     vm.currentDashboardId = $stateParams.dashboardId;
     if ($stateParams.customerId) {
         vm.currentCustomerId = $stateParams.customerId;
@@ -86,6 +88,7 @@ export default function DashboardController(types, widgetService, userService,
     vm.exportDashboard = exportDashboard;
     vm.exportWidget = exportWidget;
     vm.importWidget = importWidget;
+    vm.isPublicUser = isPublicUser;
     vm.isTenantAdmin = isTenantAdmin;
     vm.isSystemAdmin = isSystemAdmin;
     vm.loadDashboard = loadDashboard;
@@ -95,7 +98,7 @@ export default function DashboardController(types, widgetService, userService,
     vm.showDashboardToolbar = showDashboardToolbar;
     vm.onAddWidgetClosed = onAddWidgetClosed;
     vm.onEditWidgetClosed = onEditWidgetClosed;
-    vm.openDeviceAliases = openDeviceAliases;
+    vm.openEntityAliases = openEntityAliases;
     vm.openDashboardSettings = openDashboardSettings;
     vm.removeWidget = removeWidget;
     vm.saveDashboard = saveDashboard;
@@ -104,6 +107,9 @@ export default function DashboardController(types, widgetService, userService,
     vm.onRevertWidgetEdit = onRevertWidgetEdit;
     vm.helpLinkIdForWidgetType = helpLinkIdForWidgetType;
     vm.displayTitle = displayTitle;
+    vm.displayExport = displayExport;
+    vm.displayDashboardTimewindow = displayDashboardTimewindow;
+    vm.displayEntitiesSelect = displayEntitiesSelect;
 
     vm.widgetsBundle;
 
@@ -223,21 +229,8 @@ export default function DashboardController(types, widgetService, userService,
 
             dashboardService.getDashboard($stateParams.dashboardId)
                 .then(function success(dashboard) {
-                    vm.dashboard = dashboard;
-                    if (vm.dashboard.configuration == null) {
-                        vm.dashboard.configuration = {widgets: [], deviceAliases: {}};
-                    }
-                    if (angular.isUndefined(vm.dashboard.configuration.widgets)) {
-                        vm.dashboard.configuration.widgets = [];
-                    }
-                    if (angular.isUndefined(vm.dashboard.configuration.deviceAliases)) {
-                        vm.dashboard.configuration.deviceAliases = {};
-                    }
-
-                    if (angular.isUndefined(vm.dashboard.configuration.timewindow)) {
-                        vm.dashboard.configuration.timewindow = timeService.defaultTimewindow();
-                    }
-                    deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
+                    vm.dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
+                    entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
                         .then(
                             function(resolution) {
                                 if (resolution.error && !isTenantAdmin()) {
@@ -273,6 +266,10 @@ export default function DashboardController(types, widgetService, userService,
         vm.dashboardInitComplete = true;
     }
 
+    function isPublicUser() {
+        return vm.user.isPublic === true;
+    }
+
     function isTenantAdmin() {
         return vm.user.authority === 'TENANT_ADMIN';
     }
@@ -293,26 +290,26 @@ export default function DashboardController(types, widgetService, userService,
         return vm.dashboardInitComplete;
     }
 
-    function openDeviceAliases($event) {
+    function openEntityAliases($event) {
         $mdDialog.show({
-            controller: 'DeviceAliasesController',
+            controller: 'EntityAliasesController',
             controllerAs: 'vm',
-            templateUrl: deviceAliasesTemplate,
+            templateUrl: entityAliasesTemplate,
             locals: {
                 config: {
-                    deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+                    entityAliases: angular.copy(vm.dashboard.configuration.entityAliases),
                     widgets: vm.widgets,
-                    isSingleDeviceAlias: false,
-                    singleDeviceAlias: null
+                    isSingleEntityAlias: false,
+                    singleEntityAlias: null
                 }
             },
             parent: angular.element($document[0].body),
             skipHide: true,
             fullscreen: true,
             targetEvent: $event
-        }).then(function (deviceAliases) {
-            vm.dashboard.configuration.deviceAliases = deviceAliases;
-            deviceAliasesUpdated();
+        }).then(function (entityAliases) {
+            vm.dashboard.configuration.entityAliases = entityAliases;
+            entityAliasesUpdated();
         }, function () {
         });
     }
@@ -382,7 +379,7 @@ export default function DashboardController(types, widgetService, userService,
 
     function importWidget($event) {
         $event.stopPropagation();
-        importExport.importWidget($event, vm.dashboard, deviceAliasesUpdated);
+        importExport.importWidget($event, vm.dashboard, entityAliasesUpdated);
     }
 
     function widgetMouseDown($event, widget) {
@@ -463,9 +460,9 @@ export default function DashboardController(types, widgetService, userService,
             );
             dashboardContextActions.push(
                 {
-                    action: openDeviceAliases,
+                    action: openEntityAliases,
                     enabled: true,
-                    value: "device.aliases",
+                    value: "entity.aliases",
                     icon: "devices_other"
                 }
             );
@@ -484,7 +481,7 @@ export default function DashboardController(types, widgetService, userService,
 
     function pasteWidget($event) {
         var pos = vm.dashboardContainer.getEventGridPosition($event);
-        itembuffer.pasteWidget(vm.dashboard, pos, deviceAliasesUpdated);
+        itembuffer.pasteWidget(vm.dashboard, pos, entityAliasesUpdated);
     }
 
     function prepareWidgetContextMenu() {
@@ -560,6 +557,33 @@ export default function DashboardController(types, widgetService, userService,
         }
     }
 
+    function displayExport() {
+        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
+            angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardExport)) {
+            return vm.dashboard.configuration.gridSettings.showDashboardExport;
+        } else {
+            return true;
+        }
+    }
+
+    function displayDashboardTimewindow() {
+        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
+            angular.isDefined(vm.dashboard.configuration.gridSettings.showDashboardTimewindow)) {
+            return vm.dashboard.configuration.gridSettings.showDashboardTimewindow;
+        } else {
+            return true;
+        }
+    }
+
+    function displayEntitiesSelect() {
+        if (vm.dashboard && vm.dashboard.configuration.gridSettings &&
+            angular.isDefined(vm.dashboard.configuration.gridSettings.showEntitiesSelect)) {
+            return vm.dashboard.configuration.gridSettings.showEntitiesSelect;
+        } else {
+            return true;
+        }
+    }
+
     function onRevertWidgetEdit(widgetForm) {
         if (widgetForm.$dirty) {
             widgetForm.$setPristine();
@@ -617,22 +641,8 @@ export default function DashboardController(types, widgetService, userService,
                     sizeY: widgetTypeInfo.sizeY,
                     config: config
                 };
-                $mdDialog.show({
-                    controller: 'AddWidgetController',
-                    controllerAs: 'vm',
-                    templateUrl: addWidgetTemplate,
-                    locals: {dashboard: vm.dashboard, aliasesInfo: vm.aliasesInfo, widget: newWidget, widgetInfo: widgetTypeInfo},
-                    parent: angular.element($document[0].body),
-                    fullscreen: true,
-                    skipHide: true,
-                    targetEvent: event,
-                    onComplete: function () {
-                        var w = angular.element($window);
-                        w.triggerHandler('resize');
-                    }
-                }).then(function (result) {
-                    var widget = result.widget;
-                    vm.aliasesInfo = result.aliasesInfo;
+
+                function addWidget(widget) {
                     var columns = 24;
                     if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
                         columns = vm.dashboard.configuration.gridSettings.columns;
@@ -643,9 +653,37 @@ export default function DashboardController(types, widgetService, userService,
                         widget.sizeY *= ratio;
                     }
                     vm.widgets.push(widget);
-                }, function (rejection) {
-                    vm.aliasesInfo = rejection.aliasesInfo;
-                });
+                }
+
+                if (widgetTypeInfo.useCustomDatasources) {
+                    addWidget(newWidget);
+                } else {
+                    $mdDialog.show({
+                        controller: 'AddWidgetController',
+                        controllerAs: 'vm',
+                        templateUrl: addWidgetTemplate,
+                        locals: {
+                            dashboard: vm.dashboard,
+                            aliasesInfo: vm.aliasesInfo,
+                            widget: newWidget,
+                            widgetInfo: widgetTypeInfo
+                        },
+                        parent: angular.element($document[0].body),
+                        fullscreen: true,
+                        skipHide: true,
+                        targetEvent: event,
+                        onComplete: function () {
+                            var w = angular.element($window);
+                            w.triggerHandler('resize');
+                        }
+                    }).then(function (result) {
+                        var widget = result.widget;
+                        vm.aliasesInfo = result.aliasesInfo;
+                        addWidget(widget);
+                    }, function (rejection) {
+                        vm.aliasesInfo = rejection.aliasesInfo;
+                    });
+                }
             }
         );
     }
@@ -688,7 +726,7 @@ export default function DashboardController(types, widgetService, userService,
                     vm.dashboard = vm.prevDashboard;
                     vm.widgets = vm.dashboard.configuration.widgets;
                     vm.dashboardConfiguration = vm.dashboard.configuration;
-                    deviceAliasesUpdated();
+                    entityAliasesUpdated();
                 }
             }
         }
@@ -717,8 +755,8 @@ export default function DashboardController(types, widgetService, userService,
         $mdDialog.show(alert);
     }
 
-    function deviceAliasesUpdated() {
-        deviceService.processDeviceAliases(vm.dashboard.configuration.deviceAliases)
+    function entityAliasesUpdated() {
+        entityService.processEntityAliases(vm.dashboard.configuration.entityAliases)
             .then(
                 function(resolution) {
                     if (resolution.aliasesInfo) {
diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js
index 2e9f55a..b2f3117 100644
--- a/ui/src/app/dashboard/dashboard.directive.js
+++ b/ui/src/app/dashboard/dashboard.directive.js
@@ -20,30 +20,44 @@ import dashboardFieldsetTemplate from './dashboard-fieldset.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DashboardDirective($compile, $templateCache, types, customerService) {
+export default function DashboardDirective($compile, $templateCache, $translate, types, toast, customerService, dashboardService) {
     var linker = function (scope, element) {
         var template = $templateCache.get(dashboardFieldsetTemplate);
         element.html(template);
 
         scope.isAssignedToCustomer = false;
+        scope.isPublic = false;
         scope.assignedCustomer = null;
+        scope.publicLink = null;
 
         scope.$watch('dashboard', function(newVal) {
             if (newVal) {
                 if (scope.dashboard.customerId && scope.dashboard.customerId.id !== types.id.nullUid) {
                     scope.isAssignedToCustomer = true;
-                    customerService.getCustomer(scope.dashboard.customerId.id).then(
+                    customerService.getShortCustomerInfo(scope.dashboard.customerId.id).then(
                         function success(customer) {
                             scope.assignedCustomer = customer;
+                            scope.isPublic = customer.isPublic;
+                            if (scope.isPublic) {
+                                scope.publicLink = dashboardService.getPublicDashboardLink(scope.dashboard);
+                            } else {
+                                scope.publicLink = null;
+                            }
                         }
                     );
                 } else {
                     scope.isAssignedToCustomer = false;
+                    scope.isPublic = false;
+                    scope.publicLink = null;
                     scope.assignedCustomer = null;
                 }
             }
         });
 
+        scope.onPublicLinkCopied = function() {
+            toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
         $compile(element.contents())(scope);
     }
     return {
@@ -55,6 +69,7 @@ export default function DashboardDirective($compile, $templateCache, types, cust
             dashboardScope: '=',
             theForm: '=',
             onAssignToCustomer: '&',
+            onMakePublic: '&',
             onUnassignFromCustomer: '&',
             onExportDashboard: '&',
             onDeleteDashboard: '&'
diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js
index 2c400ea..e9fe1f2 100644
--- a/ui/src/app/dashboard/dashboard.routes.js
+++ b/ui/src/app/dashboard/dashboard.routes.js
@@ -62,7 +62,7 @@ export default function DashboardRoutes($stateProvider) {
                 pageTitle: 'customer.dashboards'
             },
             ncyBreadcrumb: {
-                label: '{"icon": "dashboard", "label": "customer.dashboards"}'
+                label: '{"icon": "dashboard", "label": "{{ vm.customerDashboardsTitle }}", "translate": "false"}'
             }
         })
         .state('home.dashboards.dashboard', {
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index ad9e52b..3286f8d 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -47,28 +47,33 @@
                                aria-label="{{ 'fullscreen.fullscreen' | translate }}"
                                class="md-icon-button">
                     </md-button>
-                    <tb-user-menu ng-show="forceFullscreen" display-user-info="true">
+                    <tb-user-menu ng-if="!vm.isPublicUser() && forceFullscreen" display-user-info="true">
                     </tb-user-menu>
-                    <md-button aria-label="{{ 'action.export' | translate }}" class="md-icon-button"
+                    <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 is-toolbar direction="left" tooltip-direction="bottom" aggregation ng-model="vm.dashboardConfiguration.timewindow">
+                    <tb-timewindow ng-show="vm.isEdit || vm.displayDashboardTimewindow()"
+                                   is-toolbar
+                                   direction="left"
+                                   tooltip-direction="bottom" aggregation
+                                   ng-model="vm.dashboardConfiguration.timewindow">
                     </tb-timewindow>
-                    <tb-aliases-device-select ng-show="!vm.isEdit"
+                    <tb-aliases-entity-select ng-show="!vm.isEdit && vm.displayEntitiesSelect()"
                                               tooltip-direction="bottom"
-                                              ng-model="vm.aliasesInfo.deviceAliases"
-                                              device-aliases-info="vm.aliasesInfo.deviceAliasesInfo">
-                    </tb-aliases-device-select>
-                    <md-button ng-show="vm.isEdit" aria-label="{{ 'device.aliases' | translate }}" class="md-icon-button"
-                               ng-click="vm.openDeviceAliases($event)">
+                                              ng-model="vm.aliasesInfo.entityAliases"
+                                              entity-aliases-info="vm.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">
-                            {{ 'device.aliases' | translate }}
+                            {{ 'entity.aliases' | translate }}
                         </md-tooltip>
-                        <md-icon aria-label="{{ 'device.aliases' | translate }}" class="material-icons">devices_other</md-icon>
+                        <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)">
@@ -304,6 +309,6 @@
         </section>
     </section>
     <section class="tb-powered-by-footer" ng-style="{'color': vm.dashboard.configuration.gridSettings.titleColor}">
-        <span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard</a></span>
+        <span>Powered by <a href="https://thingsboard.io" target="_blank">Thingsboard v.{{ vm.thingsboardVersion }}</a></span>
     </section>
 </md-content>
diff --git a/ui/src/app/dashboard/dashboard-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html
index 9f6cd00..6367867 100644
--- a/ui/src/app/dashboard/dashboard-card.tpl.html
+++ b/ui/src/app/dashboard/dashboard-card.tpl.html
@@ -15,5 +15,6 @@
     limitations under the License.
 
 -->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</div>
+<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'dashboard.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
+<div class="tb-small" ng-show="vm.isPublic()">{{'dashboard.public' | translate}}</div>
 
diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
index 6325e17..954d881 100644
--- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html
+++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
@@ -15,25 +15,50 @@
     limitations under the License.
 
 -->
+<md-button ng-click="onExportDashboard({event: $event})"
+		   ng-show="!isEdit && dashboardScope === 'tenant'"
+		   class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
+<md-button ng-click="onMakePublic({event: $event})"
+		   ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer && !isPublic"
+		   class="md-raised md-primary">{{ 'dashboard.make-public' | translate }}</md-button>
 <md-button ng-click="onAssignToCustomer({event: $event})"
 		   ng-show="!isEdit && dashboardScope === 'tenant' && !isAssignedToCustomer"
 		   class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
-<md-button ng-click="onUnassignFromCustomer({event: $event})"
+<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
 		   ng-show="!isEdit && (dashboardScope === 'customer' || dashboardScope === 'tenant') && isAssignedToCustomer"
-		   class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button>
-<md-button ng-click="onExportDashboard({event: $event})"
-		   ng-show="!isEdit && dashboardScope === 'tenant'"
-		   class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
+		   class="md-raised md-primary">{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }}</md-button>
 <md-button ng-click="onDeleteDashboard({event: $event})"
 		   ng-show="!isEdit && dashboardScope === 'tenant'"
 		   class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
-
 <md-content class="md-padding" layout="column">
 	<md-input-container class="md-block"
-						ng-show="isAssignedToCustomer && dashboardScope === 'tenant'">
+						ng-show="!isEdit && isAssignedToCustomer && !isPublic && dashboardScope === 'tenant'">
 		<label translate>dashboard.assignedToCustomer</label>
 		<input ng-model="assignedCustomer.title" disabled>
 	</md-input-container>
+	<div layout="column" ng-show="!isEdit && isPublic && (dashboardScope === 'customer' || dashboardScope === 'tenant')">
+		<tb-social-share-panel style="padding-bottom: 10px;"
+							   share-title="{{ 'dashboard.socialshare-title' | translate:{dashboardTitle: dashboard.title} }}"
+							   share-text="{{ 'dashboard.socialshare-text' | translate:{dashboardTitle: dashboard.title} }}"
+							   share-link="{{ publicLink }}"
+							   share-hash-tags="thingsboard, iot">
+		</tb-social-share-panel>
+		<div layout="row">
+			<md-input-container class="md-block" flex>
+				<label translate>dashboard.public-link</label>
+				<input ng-model="publicLink" disabled>
+			</md-input-container>
+			<md-button class="md-icon-button" style="margin-top: 14px;"
+					   ngclipboard
+					   data-clipboard-text="{{ publicLink }}"
+					   ngclipboard-success="onPublicLinkCopied(e)">
+				<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+				<md-tooltip md-direction="top">
+					{{ 'dashboard.copy-public-link' | translate }}
+				</md-tooltip>
+			</md-button>
+		</div>
+	</div>
 	<fieldset ng-disabled="loading || !isEdit">
 		<md-input-container class="md-block">
 			<label translate>dashboard.title</label>
diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js
index 3ef96a7..134baff 100644
--- a/ui/src/app/dashboard/dashboards.controller.js
+++ b/ui/src/app/dashboard/dashboards.controller.js
@@ -19,11 +19,33 @@ import addDashboardTemplate from './add-dashboard.tpl.html';
 import dashboardCard from './dashboard-card.tpl.html';
 import assignToCustomerTemplate from './assign-to-customer.tpl.html';
 import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.html';
+import makeDashboardPublicDialogTemplate from './make-dashboard-public-dialog.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export function DashboardCardController($scope, types, customerService) {
+export function MakeDashboardPublicDialogController($mdDialog, $translate, toast, dashboardService, dashboard) {
+
+    var vm = this;
+
+    vm.dashboard = dashboard;
+    vm.publicLink = dashboardService.getPublicDashboardLink(dashboard);
+
+    vm.onPublicLinkCopied = onPublicLinkCopied;
+    vm.close = close;
+
+    function onPublicLinkCopied(){
+        toast.showSuccess($translate.instant('dashboard.public-link-copied-message'), 750, angular.element('#make-dialog-public-content'), 'bottom left');
+    }
+
+    function close() {
+        $mdDialog.hide();
+    }
+
+}
+
+/*@ngInject*/
+export function DashboardCardController(types) {
 
     var vm = this;
 
@@ -31,27 +53,22 @@ export function DashboardCardController($scope, types, customerService) {
 
     vm.isAssignedToCustomer = function() {
         if (vm.item && vm.item.customerId && vm.parentCtl.dashboardsScope === 'tenant' &&
-            vm.item.customerId.id != vm.types.id.nullUid) {
+            vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
             return true;
         }
         return false;
     }
 
-    $scope.$watch('vm.item',
-        function() {
-            if (vm.isAssignedToCustomer()) {
-                customerService.getCustomerTitle(vm.item.customerId.id).then(
-                    function success(title) {
-                        vm.customerTitle = title;
-                    }
-                );
-            }
+    vm.isPublic = function() {
+        if (vm.item && vm.item.assignedCustomer && vm.parentCtl.dashboardsScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
+            return true;
         }
-    );
+        return false;
+    }
 }
 
 /*@ngInject*/
-export function DashboardsController(userService, dashboardService, customerService, importExport, types, $scope, $controller,
+export function DashboardsController(userService, dashboardService, customerService, importExport, types,
                                              $state, $stateParams, $mdDialog, $document, $q, $translate) {
 
     var customerId = $stateParams.customerId;
@@ -119,6 +136,7 @@ export function DashboardsController(userService, dashboardService, customerServ
     vm.dashboardsScope = $state.$current.data.dashboardsType;
 
     vm.assignToCustomer = assignToCustomer;
+    vm.makePublic = makePublic;
     vm.unassignFromCustomer = unassignFromCustomer;
     vm.exportDashboard = exportDashboard;
 
@@ -136,6 +154,17 @@ export function DashboardsController(userService, dashboardService, customerServ
             customerId = user.customerId;
         }
 
+        if (customerId) {
+            vm.customerDashboardsTitle = $translate.instant('customer.dashboards');
+            customerService.getShortCustomerInfo(customerId).then(
+                function success(info) {
+                    if (info.isPublic) {
+                        vm.customerDashboardsTitle = $translate.instant('customer.public-dashboards');
+                    }
+                }
+            );
+        }
+
         if (vm.dashboardsScope === 'tenant') {
             fetchDashboardsFunction = function (pageLink) {
                 return dashboardService.getTenantDashboards(pageLink);
@@ -155,8 +184,21 @@ export function DashboardsController(userService, dashboardService, customerServ
                     name: function() { $translate.instant('action.export') },
                     details: function() { return $translate.instant('dashboard.export') },
                     icon: "file_download"
-                },
-                {
+                });
+
+            dashboardActionsList.push({
+                    onAction: function ($event, item) {
+                        makePublic($event, item);
+                    },
+                    name: function() { return $translate.instant('action.share') },
+                    details: function() { return $translate.instant('dashboard.make-public') },
+                    icon: "share",
+                    isEnabled: function(dashboard) {
+                        return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
+                    }
+                });
+
+            dashboardActionsList.push({
                     onAction: function ($event, item) {
                         assignToCustomer($event, [ item.id.id ]);
                     },
@@ -166,19 +208,29 @@ export function DashboardsController(userService, dashboardService, customerServ
                     isEnabled: function(dashboard) {
                         return dashboard && (!dashboard.customerId || dashboard.customerId.id === types.id.nullUid);
                     }
-                },
-                {
+                });
+            dashboardActionsList.push({
                     onAction: function ($event, item) {
-                        unassignFromCustomer($event, item);
+                        unassignFromCustomer($event, item, false);
                     },
                     name: function() { return $translate.instant('action.unassign') },
                     details: function() { return $translate.instant('dashboard.unassign-from-customer') },
                     icon: "assignment_return",
                     isEnabled: function(dashboard) {
-                        return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid;
+                        return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && !dashboard.assignedCustomer.isPublic;
                     }
-                }
-            );
+                });
+            dashboardActionsList.push({
+                    onAction: function ($event, item) {
+                        unassignFromCustomer($event, item, true);
+                    },
+                    name: function() { return $translate.instant('action.make-private') },
+                    details: function() { return $translate.instant('dashboard.make-private') },
+                    icon: "reply",
+                    isEnabled: function(dashboard) {
+                        return dashboard && dashboard.customerId && dashboard.customerId.id !== types.id.nullUid && dashboard.assignedCustomer.isPublic;
+                    }
+                });
 
             dashboardActionsList.push(
                 {
@@ -262,11 +314,27 @@ export function DashboardsController(userService, dashboardService, customerServ
                 dashboardActionsList.push(
                     {
                         onAction: function ($event, item) {
-                            unassignFromCustomer($event, item);
+                            unassignFromCustomer($event, item, false);
                         },
                         name: function() { return $translate.instant('action.unassign') },
                         details: function() { return $translate.instant('dashboard.unassign-from-customer') },
-                        icon: "assignment_return"
+                        icon: "assignment_return",
+                        isEnabled: function(dashboard) {
+                            return dashboard && !dashboard.assignedCustomer.isPublic;
+                        }
+                    }
+                );
+                dashboardActionsList.push(
+                    {
+                        onAction: function ($event, item) {
+                            unassignFromCustomer($event, item, true);
+                        },
+                        name: function() { return $translate.instant('action.make-private') },
+                        details: function() { return $translate.instant('dashboard.make-private') },
+                        icon: "reply",
+                        isEnabled: function(dashboard) {
+                            return dashboard && dashboard.assignedCustomer.isPublic;
+                        }
                     }
                 );
 
@@ -336,7 +404,28 @@ export function DashboardsController(userService, dashboardService, customerServ
     }
 
     function saveDashboard(dashboard) {
-        return dashboardService.saveDashboard(dashboard);
+        var deferred = $q.defer();
+        dashboardService.saveDashboard(dashboard).then(
+            function success(savedDashboard) {
+                var dashboards = [ savedDashboard ];
+                customerService.applyAssignedCustomersInfo(dashboards).then(
+                    function success(items) {
+                        if (items && items.length == 1) {
+                            deferred.resolve(items[0]);
+                        } else {
+                            deferred.reject();
+                        }
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
     }
 
     function assignToCustomer($event, dashboardIds) {
@@ -418,15 +507,27 @@ export function DashboardsController(userService, dashboardService, customerServ
         assignToCustomer($event, dashboardIds);
     }
 
-    function unassignFromCustomer($event, dashboard) {
+    function unassignFromCustomer($event, dashboard, isPublic) {
         if ($event) {
             $event.stopPropagation();
         }
+        var title;
+        var content;
+        var label;
+        if (isPublic) {
+            title = $translate.instant('dashboard.make-private-dashboard-title', {dashboardTitle: dashboard.title});
+            content = $translate.instant('dashboard.make-private-dashboard-text');
+            label = $translate.instant('dashboard.make-private-dashboard');
+        } else {
+            title = $translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title});
+            content = $translate.instant('dashboard.unassign-dashboard-text');
+            label = $translate.instant('dashboard.unassign-dashboard');
+        }
         var confirm = $mdDialog.confirm()
             .targetEvent($event)
-            .title($translate.instant('dashboard.unassign-dashboard-title', {dashboardTitle: dashboard.title}))
-            .htmlContent($translate.instant('dashboard.unassign-dashboard-text'))
-            .ariaLabel($translate.instant('dashboard.unassign-dashboard'))
+            .title(title)
+            .htmlContent(content)
+            .ariaLabel(label)
             .cancel($translate.instant('action.no'))
             .ok($translate.instant('action.yes'));
         $mdDialog.show(confirm).then(function () {
@@ -436,6 +537,25 @@ export function DashboardsController(userService, dashboardService, customerServ
         });
     }
 
+    function makePublic($event, dashboard) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        dashboardService.makeDashboardPublic(dashboard.id.id).then(function success(dashboard) {
+            $mdDialog.show({
+                controller: 'MakeDashboardPublicDialogController',
+                controllerAs: 'vm',
+                templateUrl: makeDashboardPublicDialogTemplate,
+                locals: {dashboard: dashboard},
+                parent: angular.element($document[0].body),
+                fullscreen: true,
+                targetEvent: $event
+            }).then(function () {
+                vm.grid.refreshList();
+            });
+        });
+    }
+
     function exportDashboard($event, dashboard) {
         $event.stopPropagation();
         importExport.exportDashboard(dashboard.id.id);
diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html
index bae57b9..dde2f86 100644
--- a/ui/src/app/dashboard/dashboards.tpl.html
+++ b/ui/src/app/dashboard/dashboards.tpl.html
@@ -24,7 +24,8 @@
 						  dashboard-scope="vm.dashboardsScope"
 						  the-form="vm.grid.detailsForm"
 						  on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
-						  on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
+						  on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
+						  on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
 						  on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
 						  on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
 </tb-grid>
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
index aac6da3..ee107cc 100644
--- a/ui/src/app/dashboard/dashboard-settings.controller.js
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -31,6 +31,18 @@ export default function DashboardSettingsController($scope, $mdDialog, gridSetti
         vm.gridSettings.showTitle = true;
     }
 
+    if (angular.isUndefined(vm.gridSettings.showEntitiesSelect)) {
+        vm.gridSettings.showEntitiesSelect = true;
+    }
+
+    if (angular.isUndefined(vm.gridSettings.showDashboardTimewindow)) {
+        vm.gridSettings.showDashboardTimewindow = true;
+    }
+
+    if (angular.isUndefined(vm.gridSettings.showDashboardExport)) {
+        vm.gridSettings.showDashboardExport = true;
+    }
+
     vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
     vm.gridSettings.titleColor = vm.gridSettings.titleColor || 'rgba(0,0,0,0.870588)';
     vm.gridSettings.columns = vm.gridSettings.columns || 24;
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
index 2ed25d3..6ae4746 100644
--- a/ui/src/app/dashboard/dashboard-settings.tpl.html
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -48,6 +48,17 @@
                              md-color-history="false"
                         ></div>
                     </div>
+                    <div layout="row" layout-align="start center">
+                        <md-checkbox flex aria-label="{{ 'dashboard.display-entities-selection' | translate }}"
+                                     ng-model="vm.gridSettings.showEntitiesSelect">{{ 'dashboard.display-entities-selection' | translate }}
+                        </md-checkbox>
+                        <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-timewindow' | translate }}"
+                                     ng-model="vm.gridSettings.showDashboardTimewindow">{{ 'dashboard.display-dashboard-timewindow' | translate }}
+                        </md-checkbox>
+                        <md-checkbox flex aria-label="{{ 'dashboard.display-dashboard-export' | translate }}"
+                                     ng-model="vm.gridSettings.showDashboardExport">{{ 'dashboard.display-dashboard-export' | translate }}
+                        </md-checkbox>
+                    </div>
                     <md-input-container class="md-block">
                         <label translate>dashboard.columns-count</label>
                         <input required type="number" step="any" name="columns" ng-model="vm.gridSettings.columns" min="10"
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 1c2e461..1256065 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -15,13 +15,13 @@
  */
 /* eslint-disable import/no-unresolved, import/default */
 
-import deviceAliasesTemplate from './device-aliases.tpl.html';
+import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
 import editWidgetTemplate from './edit-widget.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function EditWidgetDirective($compile, $templateCache, widgetService, deviceService, $q, $document, $mdDialog) {
+export default function EditWidgetDirective($compile, $templateCache, types, widgetService, entityService, $q, $document, $mdDialog) {
 
     var linker = function (scope, element) {
         var template = $templateCache.get(editWidgetTemplate);
@@ -37,6 +37,7 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
                             scope.widgetConfig = scope.widget.config;
                             var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
                             var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
+                            scope.isDataEnabled = !widgetInfo.useCustomDatasources;
                             if (!settingsSchema || settingsSchema === '') {
                                 scope.settingsSchema = {};
                             } else {
@@ -57,45 +58,46 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
             }
         });
 
-        scope.fetchDeviceKeys = function (deviceAliasId, query, type) {
-            var deviceAlias = scope.aliasesInfo.deviceAliases[deviceAliasId];
-            if (deviceAlias && deviceAlias.deviceId) {
-                return deviceService.getDeviceKeys(deviceAlias.deviceId, query, type);
+        scope.fetchEntityKeys = function (entityAliasId, query, type) {
+            var entityAlias = scope.aliasesInfo.entityAliases[entityAliasId];
+            if (entityAlias && entityAlias.entityId) {
+                return entityService.getEntityKeys(entityAlias.entityType, entityAlias.entityId, query, type);
             } else {
                 return $q.when([]);
             }
         };
 
-        scope.createDeviceAlias = function (event, alias) {
+        scope.createEntityAlias = function (event, alias, allowedEntityTypes) {
 
             var deferred = $q.defer();
-            var singleDeviceAlias = {id: null, alias: alias, deviceFilter: null};
+            var singleEntityAlias = {id: null, alias: alias, entityType: types.entityType.device, entityFilter: null};
 
             $mdDialog.show({
-                controller: 'DeviceAliasesController',
+                controller: 'EntityAliasesController',
                 controllerAs: 'vm',
-                templateUrl: deviceAliasesTemplate,
+                templateUrl: entityAliasesTemplate,
                 locals: {
                     config: {
-                        deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
+                        entityAliases: angular.copy(scope.dashboard.configuration.entityAliases),
                         widgets: null,
-                        isSingleDeviceAlias: true,
-                        singleDeviceAlias: singleDeviceAlias
+                        isSingleEntityAlias: true,
+                        singleEntityAlias: singleEntityAlias,
+                        allowedEntityTypes: allowedEntityTypes
                     }
                 },
                 parent: angular.element($document[0].body),
                 fullscreen: true,
                 skipHide: true,
                 targetEvent: event
-            }).then(function (singleDeviceAlias) {
-                scope.dashboard.configuration.deviceAliases[singleDeviceAlias.id] =
-                            { alias: singleDeviceAlias.alias, deviceFilter: singleDeviceAlias.deviceFilter };
-                deviceService.processDeviceAliases(scope.dashboard.configuration.deviceAliases).then(
+            }).then(function (singleEntityAlias) {
+                scope.dashboard.configuration.entityAliases[singleEntityAlias.id] =
+                            { alias: singleEntityAlias.alias, entityType: singleEntityAlias.entityType, entityFilter: singleEntityAlias.entityFilter };
+                entityService.processEntityAliases(scope.dashboard.configuration.entityAliases).then(
                     function(resolution) {
                         if (!resolution.error) {
                             scope.aliasesInfo = resolution.aliasesInfo;
                         }
-                        deferred.resolve(singleDeviceAlias);
+                        deferred.resolve(singleEntityAlias);
                     }
                 );
             }, function () {
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
index 7aa6339..279d311 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -18,11 +18,12 @@
 <fieldset ng-disabled="loading">
 	<tb-widget-config widget-type="widget.type"
 					  ng-model="widgetConfig"
+					  is-data-enabled="isDataEnabled"
 					  widget-settings-schema="settingsSchema"
 					  datakey-settings-schema="dataKeySettingsSchema"
-					  device-aliases="aliasesInfo.deviceAliases"
+					  entity-aliases="aliasesInfo.entityAliases"
 					  functions-only="functionsOnly"
-					  fetch-device-keys="fetchDeviceKeys(deviceAliasId, query, type)"
-					  on-create-device-alias="createDeviceAlias(event, alias)"
+					  fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
+					  on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
 					  the-form="theForm"></tb-widget-config>
 </fieldset>
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index dc74c76..c8ba734 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -24,28 +24,25 @@ import thingsboardApiUser from '../api/user.service';
 import thingsboardApiDashboard from '../api/dashboard.service';
 import thingsboardApiCustomer from '../api/customer.service';
 import thingsboardDetailsSidenav from '../components/details-sidenav.directive';
-import thingsboardDeviceFilter from '../components/device-filter.directive';
 import thingsboardWidgetConfig from '../components/widget-config.directive';
 import thingsboardDashboardSelect from '../components/dashboard-select.directive';
 import thingsboardDashboard from '../components/dashboard.directive';
 import thingsboardExpandFullscreen from '../components/expand-fullscreen.directive';
 import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
+import thingsboardSocialsharePanel from '../components/socialshare-panel.directive';
 import thingsboardTypes from '../common/types.constant';
 import thingsboardItemBuffer from '../services/item-buffer.service';
 import thingsboardImportExport from '../import-export';
 
 import DashboardRoutes from './dashboard.routes';
-import {DashboardsController, DashboardCardController} from './dashboards.controller';
+import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller';
 import DashboardController from './dashboard.controller';
-import DeviceAliasesController from './device-aliases.controller';
-import AliasesDeviceSelectPanelController from './aliases-device-select-panel.controller';
 import DashboardSettingsController from './dashboard-settings.controller';
 import AssignDashboardToCustomerController from './assign-to-customer.controller';
 import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller';
 import AddWidgetController from './add-widget.controller';
 import DashboardDirective from './dashboard.directive';
 import EditWidgetDirective from './edit-widget.directive';
-import AliasesDeviceSelectDirective from './aliases-device-select.directive';
 
 export default angular.module('thingsboard.dashboard', [
     uiRouter,
@@ -59,24 +56,22 @@ export default angular.module('thingsboard.dashboard', [
     thingsboardApiDashboard,
     thingsboardApiCustomer,
     thingsboardDetailsSidenav,
-    thingsboardDeviceFilter,
     thingsboardWidgetConfig,
     thingsboardDashboardSelect,
     thingsboardDashboard,
     thingsboardExpandFullscreen,
-    thingsboardWidgetsBundleSelect
+    thingsboardWidgetsBundleSelect,
+    thingsboardSocialsharePanel
 ])
     .config(DashboardRoutes)
     .controller('DashboardsController', DashboardsController)
     .controller('DashboardCardController', DashboardCardController)
+    .controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController)
     .controller('DashboardController', DashboardController)
-    .controller('DeviceAliasesController', DeviceAliasesController)
-    .controller('AliasesDeviceSelectPanelController', AliasesDeviceSelectPanelController)
     .controller('DashboardSettingsController', DashboardSettingsController)
     .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
     .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
     .controller('AddWidgetController', AddWidgetController)
     .directive('tbDashboardDetails', DashboardDirective)
     .directive('tbEditWidget', EditWidgetDirective)
-    .directive('tbAliasesDeviceSelect', AliasesDeviceSelectDirective)
     .name;
diff --git a/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html b/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html
new file mode 100644
index 0000000..8d96b30
--- /dev/null
+++ b/ui/src/app/dashboard/make-dashboard-public-dialog.tpl.html
@@ -0,0 +1,62 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'dashboard.make-public' | translate }}" style="min-width: 400px;">
+    <form>
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate="dashboard.public-dashboard-title"></h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.close()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-dialog-content>
+            <div id="make-dialog-public-content" class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <span translate="dashboard.public-dashboard-text" translate-values="{dashboardTitle: vm.dashboard.title, publicLink: vm.publicLink}"></span>
+                    <div layout="row" layout-align="start center">
+                        <pre class="tb-highlight" flex><code>{{ vm.publicLink }}</code></pre>
+                        <md-button class="md-icon-button"
+                                   ngclipboard
+                                   data-clipboard-text="{{ vm.publicLink }}"
+                                   ngclipboard-success="vm.onPublicLinkCopied(e)">
+                            <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+                            <md-tooltip md-direction="top">
+                                {{ 'dashboard.copy-public-link' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </div>
+                    <div class="tb-notice" translate>dashboard.public-dashboard-notice</div>
+                    <tb-social-share-panel style="padding-top: 15px;"
+                            share-title="{{ 'dashboard.socialshare-title' | translate:{dashboardTitle:vm.dashboard.title} }}"
+                            share-text="{{ 'dashboard.socialshare-text' | translate:{dashboardTitle:vm.dashboard.title} }}"
+                            share-link="{{ vm.publicLink }}"
+                            share-hash-tags="thingsboard, iot">
+                    </tb-social-share-panel>
+                </md-content>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-click="vm.close()">{{ 'action.ok' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/device/add-devices-to-customer.controller.js b/ui/src/app/device/add-devices-to-customer.controller.js
index 9fd0cec..c54daab 100644
--- a/ui/src/app/device/add-devices-to-customer.controller.js
+++ b/ui/src/app/device/add-devices-to-customer.controller.js
@@ -52,7 +52,7 @@ export default function AddDevicesToCustomerController(deviceService, $mdDialog,
         fetchMoreItems_: function () {
             if (vm.devices.hasNext && !vm.devices.pending) {
                 vm.devices.pending = true;
-                deviceService.getTenantDevices(vm.devices.nextPageLink).then(
+                deviceService.getTenantDevices(vm.devices.nextPageLink, false).then(
                     function success(devices) {
                         vm.devices.data = vm.devices.data.concat(devices.data);
                         vm.devices.nextPageLink = devices.nextPageLink;
diff --git a/ui/src/app/device/attribute/attribute-table.directive.js b/ui/src/app/device/attribute/attribute-table.directive.js
index 560a4dc..701fd37 100644
--- a/ui/src/app/device/attribute/attribute-table.directive.js
+++ b/ui/src/app/device/attribute/attribute-table.directive.js
@@ -72,7 +72,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
         scope.$watch("deviceId", function(newVal, prevVal) {
             if (newVal && !angular.equals(newVal, prevVal)) {
                 scope.resetFilter();
-                scope.getDeviceAttributes();
+                scope.getDeviceAttributes(false, true);
             }
         });
 
@@ -81,7 +81,7 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
                 scope.mode = 'default';
                 scope.query.search = null;
                 scope.selectedAttributes = [];
-                scope.getDeviceAttributes();
+                scope.getDeviceAttributes(false, true);
             }
         });
 
@@ -117,15 +117,25 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
             }
         }
 
-        scope.getDeviceAttributes = function(forceUpdate) {
+        scope.onReorder = function() {
+            scope.getDeviceAttributes(false, false);
+        }
+
+        scope.onPaginate = function() {
+            scope.getDeviceAttributes(false, false);
+        }
+
+        scope.getDeviceAttributes = function(forceUpdate, reset) {
             if (scope.attributesDeferred) {
                 scope.attributesDeferred.resolve();
             }
             if (scope.deviceId && scope.attributeScope) {
-                scope.attributes = {
-                    count: 0,
-                    data: []
-                };
+                if (reset) {
+                    scope.attributes = {
+                        count: 0,
+                        data: []
+                    };
+                }
                 scope.checkSubscription();
                 scope.attributesDeferred = deviceService.getDeviceAttributes(scope.deviceId, scope.attributeScope.value,
                     scope.query, function(attributes, update, apply) {
diff --git a/ui/src/app/device/attribute/attribute-table.tpl.html b/ui/src/app/device/attribute/attribute-table.tpl.html
index 915fbea..b6099c4 100644
--- a/ui/src/app/device/attribute/attribute-table.tpl.html
+++ b/ui/src/app/device/attribute/attribute-table.tpl.html
@@ -126,7 +126,7 @@
         </md-toolbar>
         <md-table-container ng-show="mode!='widget'">
             <table md-table md-row-select multiple="" ng-model="selectedAttributes" md-progress="attributesDeferred.promise">
-                <thead md-head md-order="query.order" md-on-reorder="getDeviceAttributes">
+                <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>
@@ -147,7 +147,7 @@
         </md-table-container>
         <md-table-pagination ng-show="mode!='widget'" md-limit="query.limit" md-limit-options="[5, 10, 15]"
                              md-page="query.page" md-total="{{attributes.count}}"
-                             md-on-paginate="getDeviceAttributes" md-page-select>
+                             md-on-paginate="onPaginate" md-page-select>
         </md-table-pagination>
         <ul flex rn-carousel ng-if="mode==='widget'" class="widgets-carousel"
             rn-carousel-index="widgetsCarousel.index"
diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js
index 4de12bb..181613b 100644
--- a/ui/src/app/device/device.controller.js
+++ b/ui/src/app/device/device.controller.js
@@ -24,7 +24,7 @@ import deviceCredentialsTemplate from './device-credentials.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export function DeviceCardController($scope, types, customerService) {
+export function DeviceCardController(types) {
 
     var vm = this;
 
@@ -32,23 +32,23 @@ export function DeviceCardController($scope, types, customerService) {
 
     vm.isAssignedToCustomer = function() {
         if (vm.item && vm.item.customerId && vm.parentCtl.devicesScope === 'tenant' &&
-            vm.item.customerId.id != vm.types.id.nullUid) {
+            vm.item.customerId.id != vm.types.id.nullUid && !vm.item.assignedCustomer.isPublic) {
             return true;
         }
         return false;
     }
 
-    $scope.$watch('vm.item',
-        function() {
-            if (vm.isAssignedToCustomer()) {
-                customerService.getCustomerTitle(vm.item.customerId.id).then(
-                    function success(title) {
-                        vm.customerTitle = title;
-                    }
-                );
-            }
+    vm.isPublic = function() {
+        if (vm.item && vm.item.assignedCustomer && vm.parentCtl.devicesScope === 'tenant' && vm.item.assignedCustomer.isPublic) {
+            return true;
         }
-    );
+        return false;
+    }
+}
+
+
+/*@ngInject*/
+export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
 }
 
 
@@ -107,6 +107,7 @@ export function DeviceController(userService, deviceService, customerService, $s
     vm.devicesScope = $state.$current.data.devicesType;
 
     vm.assignToCustomer = assignToCustomer;
+    vm.makePublic = makePublic;
     vm.unassignFromCustomer = unassignFromCustomer;
     vm.manageCredentials = manageCredentials;
 
@@ -123,10 +124,20 @@ export function DeviceController(userService, deviceService, customerService, $s
             vm.devicesScope = 'customer_user';
             customerId = user.customerId;
         }
+        if (customerId) {
+            vm.customerDevicesTitle = $translate.instant('customer.devices');
+            customerService.getShortCustomerInfo(customerId).then(
+                function success(info) {
+                    if (info.isPublic) {
+                        vm.customerDevicesTitle = $translate.instant('customer.public-devices');
+                    }
+                }
+            );
+        }
 
         if (vm.devicesScope === 'tenant') {
             fetchDevicesFunction = function (pageLink) {
-                return deviceService.getTenantDevices(pageLink);
+                return deviceService.getTenantDevices(pageLink, true);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.deleteDevice(deviceId);
@@ -135,6 +146,18 @@ export function DeviceController(userService, deviceService, customerService, $s
                 return {"topIndex": vm.topIndex};
             };
 
+            deviceActionsList.push({
+                onAction: function ($event, item) {
+                    makePublic($event, item);
+                },
+                name: function() { return $translate.instant('action.share') },
+                details: function() { return $translate.instant('device.make-public') },
+                icon: "share",
+                isEnabled: function(device) {
+                    return device && (!device.customerId || device.customerId.id === types.id.nullUid);
+                }
+            });
+
             deviceActionsList.push(
                 {
                     onAction: function ($event, item) {
@@ -152,17 +175,29 @@ export function DeviceController(userService, deviceService, customerService, $s
             deviceActionsList.push(
                 {
                     onAction: function ($event, item) {
-                        unassignFromCustomer($event, item);
+                        unassignFromCustomer($event, item, false);
                     },
                     name: function() { return $translate.instant('action.unassign') },
                     details: function() { return $translate.instant('device.unassign-from-customer') },
                     icon: "assignment_return",
                     isEnabled: function(device) {
-                        return device && device.customerId && device.customerId.id !== types.id.nullUid;
+                        return device && device.customerId && device.customerId.id !== types.id.nullUid && !device.assignedCustomer.isPublic;
                     }
                 }
             );
 
+            deviceActionsList.push({
+                onAction: function ($event, item) {
+                    unassignFromCustomer($event, item, true);
+                },
+                name: function() { return $translate.instant('action.make-private') },
+                details: function() { return $translate.instant('device.make-private') },
+                icon: "reply",
+                isEnabled: function(device) {
+                    return device && device.customerId && device.customerId.id !== types.id.nullUid && device.assignedCustomer.isPublic;
+                }
+            });
+
             deviceActionsList.push(
                 {
                     onAction: function ($event, item) {
@@ -213,7 +248,7 @@ export function DeviceController(userService, deviceService, customerService, $s
 
         } else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
             fetchDevicesFunction = function (pageLink) {
-                return deviceService.getCustomerDevices(customerId, pageLink);
+                return deviceService.getCustomerDevices(customerId, pageLink, true);
             };
             deleteDeviceFunction = function (deviceId) {
                 return deviceService.unassignDeviceFromCustomer(deviceId);
@@ -226,16 +261,33 @@ export function DeviceController(userService, deviceService, customerService, $s
                 deviceActionsList.push(
                     {
                         onAction: function ($event, item) {
-                            unassignFromCustomer($event, item);
+                            unassignFromCustomer($event, item, false);
                         },
                         name: function() { return $translate.instant('action.unassign') },
                         details: function() { return $translate.instant('device.unassign-from-customer') },
-                        icon: "assignment_return"
+                        icon: "assignment_return",
+                        isEnabled: function(device) {
+                            return device && !device.assignedCustomer.isPublic;
+                        }
                     }
                 );
                 deviceActionsList.push(
                     {
                         onAction: function ($event, item) {
+                            unassignFromCustomer($event, item, true);
+                        },
+                        name: function() { return $translate.instant('action.make-private') },
+                        details: function() { return $translate.instant('device.make-private') },
+                        icon: "reply",
+                        isEnabled: function(device) {
+                            return device && device.assignedCustomer.isPublic;
+                        }
+                    }
+                );
+
+                deviceActionsList.push(
+                    {
+                        onAction: function ($event, item) {
                             manageCredentials($event, item);
                         },
                         name: function() { return $translate.instant('device.credentials') },
@@ -317,8 +369,29 @@ export function DeviceController(userService, deviceService, customerService, $s
         return device ? device.name : '';
     }
 
-    function saveDevice (device) {
-        return deviceService.saveDevice(device);
+    function saveDevice(device) {
+        var deferred = $q.defer();
+        deviceService.saveDevice(device).then(
+            function success(savedDevice) {
+                var devices = [ savedDevice ];
+                customerService.applyAssignedCustomersInfo(devices).then(
+                    function success(items) {
+                        if (items && items.length == 1) {
+                            deferred.resolve(items[0]);
+                        } else {
+                            deferred.reject();
+                        }
+                    },
+                    function fail() {
+                        deferred.reject();
+                    }
+                );
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
     }
 
     function isCustomerUser() {
@@ -365,7 +438,7 @@ export function DeviceController(userService, deviceService, customerService, $s
             $event.stopPropagation();
         }
         var pageSize = 10;
-        deviceService.getTenantDevices({limit: pageSize, textSearch: ''}).then(
+        deviceService.getTenantDevices({limit: pageSize, textSearch: ''}, false).then(
             function success(_devices) {
                 var devices = {
                     pageSize: pageSize,
@@ -404,15 +477,27 @@ export function DeviceController(userService, deviceService, customerService, $s
         assignToCustomer($event, deviceIds);
     }
 
-    function unassignFromCustomer($event, device) {
+    function unassignFromCustomer($event, device, isPublic) {
         if ($event) {
             $event.stopPropagation();
         }
+        var title;
+        var content;
+        var label;
+        if (isPublic) {
+            title = $translate.instant('device.make-private-device-title', {deviceName: device.name});
+            content = $translate.instant('device.make-private-device-text');
+            label = $translate.instant('device.make-private');
+        } else {
+            title = $translate.instant('device.unassign-device-title', {deviceName: device.name});
+            content = $translate.instant('device.unassign-device-text');
+            label = $translate.instant('device.unassign-device');
+        }
         var confirm = $mdDialog.confirm()
             .targetEvent($event)
-            .title($translate.instant('device.unassign-device-title', {deviceName: device.name}))
-            .htmlContent($translate.instant('device.unassign-device-text'))
-            .ariaLabel($translate.instant('device.unassign-device'))
+            .title(title)
+            .htmlContent(content)
+            .ariaLabel(label)
             .cancel($translate.instant('action.no'))
             .ok($translate.instant('action.yes'));
         $mdDialog.show(confirm).then(function () {
@@ -441,6 +526,24 @@ export function DeviceController(userService, deviceService, customerService, $s
         });
     }
 
+    function makePublic($event, device) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var confirm = $mdDialog.confirm()
+            .targetEvent($event)
+            .title($translate.instant('device.make-public-device-title', {deviceName: device.name}))
+            .htmlContent($translate.instant('device.make-public-device-text'))
+            .ariaLabel($translate.instant('device.make-public'))
+            .cancel($translate.instant('action.no'))
+            .ok($translate.instant('action.yes'));
+        $mdDialog.show(confirm).then(function () {
+            deviceService.makeDevicePublic(device.id.id).then(function success() {
+                vm.grid.refreshList();
+            });
+        });
+    }
+
     function manageCredentials($event, device) {
         if ($event) {
             $event.stopPropagation();
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index 9c927ae..eb50a15 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -26,6 +26,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
         element.html(template);
 
         scope.isAssignedToCustomer = false;
+        scope.isPublic = false;
         scope.assignedCustomer = null;
 
         scope.deviceCredentials = null;
@@ -41,13 +42,15 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
                 }
                 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
                     scope.isAssignedToCustomer = true;
-                    customerService.getCustomer(scope.device.customerId.id).then(
+                    customerService.getShortCustomerInfo(scope.device.customerId.id).then(
                         function success(customer) {
                             scope.assignedCustomer = customer;
+                            scope.isPublic = customer.isPublic;
                         }
                     );
                 } else {
                     scope.isAssignedToCustomer = false;
+                    scope.isPublic = false;
                     scope.assignedCustomer = null;
                 }
             }
@@ -72,6 +75,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
             deviceScope: '=',
             theForm: '=',
             onAssignToCustomer: '&',
+            onMakePublic: '&',
             onUnassignFromCustomer: '&',
             onManageCredentials: '&',
             onDeleteDevice: '&'
diff --git a/ui/src/app/device/device.routes.js b/ui/src/app/device/device.routes.js
index d0293b4..c1f5b27 100644
--- a/ui/src/app/device/device.routes.js
+++ b/ui/src/app/device/device.routes.js
@@ -61,7 +61,7 @@ export default function DeviceRoutes($stateProvider) {
                 pageTitle: 'customer.devices'
             },
             ncyBreadcrumb: {
-                label: '{"icon": "devices_other", "label": "customer.devices"}'
+                label: '{"icon": "devices_other", "label": "{{ vm.customerDevicesTitle }}", "translate": "false"}'
             }
         });
 
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index 1c667f3..bb84af7 100644
--- a/ui/src/app/device/device-card.tpl.html
+++ b/ui/src/app/device/device-card.tpl.html
@@ -15,4 +15,5 @@
     limitations under the License.
 
 -->
-<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.customerTitle}}'</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>
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index 99cedbd..fd6c9d1 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -15,12 +15,15 @@
     limitations under the License.
 
 -->
+<md-button ng-click="onMakePublic({event: $event})"
+           ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer && !isPublic"
+           class="md-raised md-primary">{{ 'device.make-public' | translate }}</md-button>
 <md-button ng-click="onAssignToCustomer({event: $event})"
            ng-show="!isEdit && deviceScope === 'tenant' && !isAssignedToCustomer"
            class="md-raised md-primary">{{ 'device.assign-to-customer' | translate }}</md-button>
-<md-button ng-click="onUnassignFromCustomer({event: $event})"
+<md-button ng-click="onUnassignFromCustomer({event: $event, isPublic: isPublic})"
            ng-show="!isEdit && (deviceScope === 'customer' || deviceScope === 'tenant') && isAssignedToCustomer"
-           class="md-raised md-primary">{{ 'device.unassign-from-customer' | translate }}</md-button>
+           class="md-raised md-primary">{{ isPublic ? 'device.make-private' : 'device.unassign-from-customer' | translate }}</md-button>
 <md-button ng-click="onManageCredentials({event: $event})"
            ng-show="!isEdit"
            class="md-raised md-primary">{{ (deviceScope === 'customer_user' ? 'device.view-credentials' : 'device.manage-credentials') | translate }}</md-button>
@@ -47,10 +50,14 @@
 
 <md-content class="md-padding" layout="column">
     <md-input-container class="md-block"
-                        ng-show="isAssignedToCustomer && deviceScope === 'tenant'">
+                        ng-show="!isEdit && isAssignedToCustomer && !isPublic && deviceScope === 'tenant'">
         <label translate>device.assignedToCustomer</label>
         <input ng-model="assignedCustomer.title" disabled>
     </md-input-container>
+    <div class="tb-small" style="padding-bottom: 10px; padding-left: 2px;"
+         ng-show="!isEdit && isPublic && (deviceScope === 'customer' || deviceScope === 'tenant')">
+        {{ 'device.device-public' | translate }}
+    </div>
 	<fieldset ng-disabled="loading || !isEdit">
 		<md-input-container class="md-block">
 			<label translate>device.name</label>
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index 52c05f2..2f90db8 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -27,21 +27,24 @@
                        device-scope="vm.devicesScope"
                        the-form="vm.grid.detailsForm"
                        on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
-                       on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
+                       on-make-public="vm.makePublic(event, vm.grid.detailsConfig.currentItem)"
+                       on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem, isPublic)"
                        on-manage-credentials="vm.manageCredentials(event, vm.grid.detailsConfig.currentItem)"
                        on-delete-device="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-device>
         </md-tab>
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
             <tb-attribute-table flex
-                            device-id="vm.grid.operatingItem().id.id"
-                            device-name="vm.grid.operatingItem().name"
-                            default-attribute-scope="{{vm.types.deviceAttributesScope.client.value}}">
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.device}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.attributesScope.client.value}}">
             </tb-attribute-table>
         </md-tab>
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
             <tb-attribute-table flex
-                                device-id="vm.grid.operatingItem().id.id"
-                                device-name="vm.grid.operatingItem().name"
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.device}}"
+                                entity-name="vm.grid.operatingItem().name"
                                 default-attribute-scope="{{vm.types.latestTelemetry.value}}"
                                 disable-attribute-scope-selection="true">
             </tb-attribute-table>
diff --git a/ui/src/app/device/index.js b/ui/src/app/device/index.js
index b84497a..ea42ee8 100644
--- a/ui/src/app/device/index.js
+++ b/ui/src/app/device/index.js
@@ -25,10 +25,7 @@ import {DeviceController, DeviceCardController} from './device.controller';
 import AssignDeviceToCustomerController from './assign-to-customer.controller';
 import AddDevicesToCustomerController from './add-devices-to-customer.controller';
 import ManageDeviceCredentialsController from './device-credentials.controller';
-import AddAttributeDialogController from './attribute/add-attribute-dialog.controller';
-import AddWidgetToDashboardDialogController from './attribute/add-widget-to-dashboard-dialog.controller';
 import DeviceDirective from './device.directive';
-import AttributeTableDirective from './attribute/attribute-table.directive';
 
 export default angular.module('thingsboard.device', [
     uiRouter,
@@ -44,8 +41,5 @@ export default angular.module('thingsboard.device', [
     .controller('AssignDeviceToCustomerController', AssignDeviceToCustomerController)
     .controller('AddDevicesToCustomerController', AddDevicesToCustomerController)
     .controller('ManageDeviceCredentialsController', ManageDeviceCredentialsController)
-    .controller('AddAttributeDialogController', AddAttributeDialogController)
-    .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
     .directive('tbDevice', DeviceDirective)
-    .directive('tbAttributeTable', AttributeTableDirective)
     .name;
diff --git a/ui/src/app/entity/aliases-entity-select.directive.js b/ui/src/app/entity/aliases-entity-select.directive.js
new file mode 100644
index 0000000..7512409
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select.directive.js
@@ -0,0 +1,154 @@
+/*
+ * 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 './aliases-entity-select.scss';
+
+import $ from 'jquery';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import aliasesEntitySelectButtonTemplate from './aliases-entity-select-button.tpl.html';
+import aliasesEntitySelectPanelTemplate from './aliases-entity-select-panel.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/* eslint-disable angular/angularelement */
+/*@ngInject*/
+export default function AliasesEntitySelectDirective($compile, $templateCache, $mdMedia, types, $mdPanel, $document, $translate) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+
+        /* tbAliasesEntitySelect (ng-model)
+         * {
+         *    "aliasId": {
+         *        alias: alias,
+         *        entityType: entityType,
+         *        entityId: entityId
+         *    }
+         * }
+         */
+
+        var template = $templateCache.get(aliasesEntitySelectButtonTemplate);
+
+        scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top';
+
+        element.html(template);
+
+        scope.openEditMode = function (event) {
+            if (scope.disabled) {
+                return;
+            }
+            var position;
+            var panelHeight = $mdMedia('min-height: 350px') ? 250 : 150;
+            var panelWidth = 300;
+            var offset = element[0].getBoundingClientRect();
+            var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
+            var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
+            var yPosition;
+            var xPosition;
+            if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
+                yPosition = $mdPanel.yPosition.ABOVE;
+            } else {
+                yPosition = $mdPanel.yPosition.BELOW;
+            }
+            if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
+                xPosition = $mdPanel.xPosition.CENTER;
+            } else {
+                xPosition = $mdPanel.xPosition.ALIGN_START;
+            }
+            position = $mdPanel.newPanelPosition()
+                .relativeTo(element)
+                .addPanelPosition(xPosition, yPosition);
+            var config = {
+                attachTo: angular.element($document[0].body),
+                controller: 'AliasesEntitySelectPanelController',
+                controllerAs: 'vm',
+                templateUrl: aliasesEntitySelectPanelTemplate,
+                panelClass: 'tb-aliases-entity-select-panel',
+                position: position,
+                fullscreen: false,
+                locals: {
+                    'entityAliases': angular.copy(scope.model),
+                    'entityAliasesInfo': scope.entityAliasesInfo,
+                    'onEntityAliasesUpdate': function (entityAliases) {
+                        scope.model = entityAliases;
+                        scope.updateView();
+                    }
+                },
+                openFrom: event,
+                clickOutsideToClose: true,
+                escapeToClose: true,
+                focusOnOpen: false
+            };
+            $mdPanel.open(config);
+        }
+
+        scope.updateView = function () {
+            var value = angular.copy(scope.model);
+            ngModelCtrl.$setViewValue(value);
+            updateDisplayValue();
+        }
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                scope.model = angular.copy(value);
+                updateDisplayValue();
+            }
+        }
+
+        function updateDisplayValue() {
+            var displayValue;
+            var singleValue = true;
+            var currentAliasId;
+            for (var aliasId in scope.model) {
+                if (!currentAliasId) {
+                    currentAliasId = aliasId;
+                } else {
+                    singleValue = false;
+                    break;
+                }
+            }
+            if (singleValue && currentAliasId) {
+                var entityId = scope.model[currentAliasId].entityId;
+                var entitiesInfo = scope.entityAliasesInfo[currentAliasId];
+                for (var i=0;i<entitiesInfo.length;i++) {
+                    if (entitiesInfo[i].id === entityId) {
+                        displayValue = entitiesInfo[i].name;
+                        break;
+                    }
+                }
+            } else {
+                displayValue = $translate.instant('entity.entities');
+            }
+            scope.displayValue = displayValue;
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            entityAliasesInfo:'='
+        },
+        link: linker
+    };
+
+}
+
+/* eslint-enable angular/angularelement */
\ No newline at end of file
diff --git a/ui/src/app/entity/aliases-entity-select.scss b/ui/src/app/entity/aliases-entity-select.scss
new file mode 100644
index 0000000..55b0442
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select.scss
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.md-panel {
+  &.tb-aliases-entity-select-panel {
+    position: absolute;
+  }
+}
+
+.tb-aliases-entity-select-panel {
+  max-height: 150px;
+  @media (min-height: 350px) {
+    max-height: 250px;
+  }
+  min-width: 300px;
+  background: white;
+  border-radius: 4px;
+  box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
+  0 13px 19px 2px rgba(0, 0, 0, 0.14),
+  0 5px 24px 4px rgba(0, 0, 0, 0.12);
+  overflow-x: hidden;
+  overflow-y: auto;
+  md-content {
+    background-color: #fff;
+  }
+}
+
+.tb-aliases-entity-select {
+  span {
+    pointer-events: all;
+    cursor: pointer;
+  }
+}
diff --git a/ui/src/app/entity/aliases-entity-select-button.tpl.html b/ui/src/app/entity/aliases-entity-select-button.tpl.html
new file mode 100644
index 0000000..b0b69f5
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select-button.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.
+
+-->
+
+<section class="tb-aliases-entity-select" layout='row' layout-align="start center" ng-style="{minHeight: '32px', padding: '0 6px'}">
+    <md-button class="md-icon-button" aria-label="{{ 'entity.select-entities' | translate }}" ng-click="openEditMode($event)">
+        <md-tooltip md-direction="{{tooltipDirection}}">
+            {{ 'entity.select-entities' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'entity.select-entities' | translate }}" class="material-icons">devices_other</md-icon>
+    </md-button>
+    <span hide-xs hide-sm ng-click="openEditMode($event)">
+        <md-tooltip md-direction="{{tooltipDirection}}">
+            {{ 'entity.select-entities' | translate }}
+        </md-tooltip>
+        {{displayValue}}
+    </span>
+</section>
diff --git a/ui/src/app/entity/aliases-entity-select-panel.controller.js b/ui/src/app/entity/aliases-entity-select-panel.controller.js
new file mode 100644
index 0000000..4128f94
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select-panel.controller.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*@ngInject*/
+export default function AliasesEntitySelectPanelController(mdPanelRef, $scope, types, entityAliases, entityAliasesInfo, onEntityAliasesUpdate) {
+
+    var vm = this;
+    vm._mdPanelRef = mdPanelRef;
+    vm.entityAliases = entityAliases;
+    vm.entityAliasesInfo = entityAliasesInfo;
+    vm.onEntityAliasesUpdate = onEntityAliasesUpdate;
+
+    $scope.$watch('vm.entityAliases', function () {
+        if (onEntityAliasesUpdate) {
+            onEntityAliasesUpdate(vm.entityAliases);
+        }
+    }, true);
+}
diff --git a/ui/src/app/entity/aliases-entity-select-panel.tpl.html b/ui/src/app/entity/aliases-entity-select-panel.tpl.html
new file mode 100644
index 0000000..d9c5f2f
--- /dev/null
+++ b/ui/src/app/entity/aliases-entity-select-panel.tpl.html
@@ -0,0 +1,33 @@
+<!--
+
+    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 layout="column">
+    <section flex layout="column">
+        <md-content flex class="md-padding" layout="column">
+            <div flex layout="row" ng-repeat="(aliasId, entityAlias) in vm.entityAliases">
+                <md-input-container flex>
+                    <label>{{entityAlias.alias}}</label>
+                    <md-select ng-model="vm.entityAliases[aliasId].entityId">
+                        <md-option ng-repeat="entityInfo in vm.entityAliasesInfo[aliasId]" ng-value="entityInfo.id">
+                            {{entityInfo.name}}
+                        </md-option>
+                    </md-select>
+                </md-input-container>
+            </div>
+        </md-content>
+    </section>
+</md-content>
diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.controller.js b/ui/src/app/entity/attribute/add-attribute-dialog.controller.js
new file mode 100644
index 0000000..af0c32e
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-attribute-dialog.controller.js
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+/*@ngInject*/
+export default function AddAttributeDialogController($scope, $mdDialog, types, attributeService, entityType, entityId, attributeScope) {
+
+    var vm = this;
+
+    vm.attribute = {};
+
+    vm.valueTypes = types.valueType;
+
+    vm.valueType = types.valueType.string;
+
+    vm.add = add;
+    vm.cancel = cancel;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function add() {
+        $scope.theForm.$setPristine();
+        attributeService.saveEntityAttributes(entityType, entityId, attributeScope, [vm.attribute]).then(
+            function success() {
+                $mdDialog.hide();
+            }
+        );
+    }
+
+    $scope.$watch('vm.valueType', function() {
+        if (vm.valueType === types.valueType.boolean) {
+            vm.attribute.value = false;
+        } else {
+            vm.attribute.value = null;
+        }
+    });
+}
diff --git a/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html b/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html
new file mode 100644
index 0000000..18f99fc
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-attribute-dialog.tpl.html
@@ -0,0 +1,95 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'attribute.add' | translate }}" style="min-width: 400px;">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>attribute.add</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <fieldset ng-disabled="loading">
+                        <md-input-container class="md-block">
+                            <label translate>attribute.key</label>
+                            <input required name="key" ng-model="vm.attribute.key">
+                            <div ng-messages="theForm.key.$error">
+                                <div translate ng-message="required">attribute.key-required</div>
+                            </div>
+                        </md-input-container>
+                        <section layout="row">
+                            <md-input-container flex="40" class="md-block" style="width: 200px;">
+                                <label translate>value.type</label>
+                                <md-select ng-model="vm.valueType" ng-disabled="loading()">
+                                    <md-option ng-repeat="type in vm.valueTypes" ng-value="type">
+                                        <md-icon md-svg-icon="{{ type.icon }}"></md-icon>
+                                        <span>{{type.name | translate}}</span>
+                                    </md-option>
+                                </md-select>
+                            </md-input-container>
+                            <md-input-container ng-if="vm.valueType===vm.valueTypes.string" flex="60" class="md-block">
+                                <label translate>value.string-value</label>
+                                <input required name="value" ng-model="vm.attribute.value">
+                                <div ng-messages="theForm.value.$error">
+                                    <div translate ng-message="required">attribute.value-required</div>
+                                </div>
+                            </md-input-container>
+                            <md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block">
+                                <label translate>value.integer-value</label>
+                                <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attribute.value">
+                                <div ng-messages="theForm.value.$error">
+                                    <div translate ng-message="required">attribute.value-required</div>
+                                    <div translate ng-message="pattern">value.invalid-integer-value</div>
+                                </div>
+                            </md-input-container>
+                            <md-input-container ng-if="vm.valueType===vm.valueTypes.double" flex="60" class="md-block">
+                                <label translate>value.double-value</label>
+                                <input required name="value" type="number" step="any" ng-model="vm.attribute.value">
+                                <div ng-messages="theForm.value.$error">
+                                    <div translate ng-message="required">attribute.value-required</div>
+                                </div>
+                            </md-input-container>
+                            <div layout="column" layout-align="center" flex="60" ng-if="vm.valueType===vm.valueTypes.boolean">
+                                <md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0px;">
+                                    {{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }}
+                                </md-checkbox>
+                            </div>
+                        </section>
+                    </fieldset>
+                </md-content>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
new file mode 100644
index 0000000..0e07cb4
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+/*@ngInject*/
+export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, entityId, entityType, entityName, widget) {
+
+    var vm = this;
+
+    vm.widget = widget;
+    vm.dashboardId = null;
+    vm.addToDashboardType = 0;
+    vm.newDashboard = {};
+    vm.openDashboard = false;
+
+    vm.add = add;
+    vm.cancel = cancel;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function add() {
+        $scope.theForm.$setPristine();
+        if (vm.addToDashboardType === 0) {
+            dashboardService.getDashboard(vm.dashboardId).then(
+                function success(dashboard) {
+                    addWidgetToDashboard(dashboard);
+                },
+                function fail() {}
+            );
+        } else {
+            addWidgetToDashboard(vm.newDashboard);
+        }
+
+    }
+
+    function addWidgetToDashboard(theDashboard) {
+        var aliasesInfo = {
+            datasourceAliases: {},
+            targetDeviceAliases: {}
+        };
+        aliasesInfo.datasourceAliases[0] = {
+            aliasName: entityName,
+            entityType: entityType,
+            entityFilter: {
+                useFilter: false,
+                entityNameFilter: '',
+                entityList: [entityId]
+            }
+        };
+        theDashboard = itembuffer.addWidgetToDashboard(theDashboard, vm.widget, aliasesInfo, null, 48, -1, -1);
+        dashboardService.saveDashboard(theDashboard).then(
+            function success(dashboard) {
+                $mdDialog.hide();
+                if (vm.openDashboard) {
+                    $state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id});
+                }
+            }
+        );
+    }
+
+}
diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
new file mode 100644
index 0000000..eb16077
--- /dev/null
+++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.tpl.html
@@ -0,0 +1,81 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog aria-label="{{ 'attribute.add-widget-to-dashboard' | translate }}" style="min-width: 400px;">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>attribute.add-widget-to-dashboard</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <md-content class="md-padding" layout="column">
+                    <fieldset ng-disabled="loading">
+                        <md-radio-group ng-model="vm.addToDashboardType" class="md-primary">
+                            <md-radio-button flex ng-value=0 class="md-primary md-align-top-left md-radio-interactive">
+                                <section flex layout="column" style="width: 300px;">
+                                    <span translate style="padding-bottom: 10px;">dashboard.select-existing</span>
+                                    <tb-dashboard-autocomplete the-form="theForm"
+                                                         ng-disabled="loading || vm.addToDashboardType != 0"
+                                                         tb-required="vm.addToDashboardType === 0"
+                                                         ng-model="vm.dashboardId"
+                                                         select-first-dashboard="false">
+                                    </tb-dashboard-autocomplete>
+                                </section>
+                            </md-radio-button>
+                            <md-radio-button flex ng-value=1 class="md-primary md-align-top-left md-radio-interactive">
+                                <section flex layout="column" style="width: 300px;">
+                                    <span translate>dashboard.create-new</span>
+                                    <md-input-container class="md-block">
+                                        <label translate>dashboard.new-dashboard-title</label>
+                                        <input ng-required="vm.addToDashboardType === 1" name="title" ng-model="vm.newDashboard.title">
+                                        <div ng-messages="theForm.title.$error">
+                                            <div translate ng-message="required">dashboard.title-required</div>
+                                        </div>
+                                    </md-input-container>
+                                </section>
+                            </md-radio-button>
+                        </md-radio-group>
+                    </fieldset>
+                </md-content>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-checkbox
+                    ng-model="vm.openDashboard"
+                    aria-label="{{ 'dashboard.open-dashboard' | translate }}"
+                    style="margin-bottom: 0px; padding-right: 20px;">
+                {{ 'dashboard.open-dashboard' | translate }}
+            </md-checkbox>
+            <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/entity/attribute/attribute-table.directive.js b/ui/src/app/entity/attribute/attribute-table.directive.js
new file mode 100644
index 0000000..e05fcb6
--- /dev/null
+++ b/ui/src/app/entity/attribute/attribute-table.directive.js
@@ -0,0 +1,433 @@
+/*
+ * 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 './attribute-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import attributeTableTemplate from './attribute-table.tpl.html';
+import addAttributeDialogTemplate from './add-attribute-dialog.tpl.html';
+import addWidgetToDashboardDialogTemplate from './add-widget-to-dashboard-dialog.tpl.html';
+import editAttributeValueTemplate from './edit-attribute-value.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import EditAttributeValueController from './edit-attribute-value.controller';
+
+/*@ngInject*/
+export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
+                                                $document, $translate, $filter, utils, types, dashboardService, attributeService, widgetService) {
+
+    var linker = function (scope, element, attrs) {
+
+        var template = $templateCache.get(attributeTableTemplate);
+
+        element.html(template);
+
+        var getAttributeScopeByValue = function(attributeScopeValue) {
+            if (scope.types.latestTelemetry.value === attributeScopeValue) {
+                return scope.types.latestTelemetry;
+            }
+            for (var attrScope in scope.attributeScopes) {
+                if (scope.attributeScopes[attrScope].value === attributeScopeValue) {
+                    return scope.attributeScopes[attrScope];
+                }
+            }
+        }
+
+        scope.types = types;
+
+        scope.entityType = attrs.entityType;
+        scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
+
+        if (scope.entityType === types.entityType.device) {
+            scope.attributeScopes = types.attributesScope;
+            scope.attributeScopeSelectionReadonly = false;
+        } else {
+            scope.attributeScopes = {};
+            scope.attributeScopes.server = types.attributesScope.server;
+            scope.attributeScopeSelectionReadonly = true;
+            if (scope.attributeScope != types.latestTelemetry) {
+                scope.attributeScope = scope.attributeScopes.server.value;
+            }
+        }
+
+        scope.attributes = {
+            count: 0,
+            data: []
+        };
+
+        scope.selectedAttributes = [];
+        scope.mode = 'default'; // 'widget'
+        scope.subscriptionId = null;
+
+        scope.query = {
+            order: 'key',
+            limit: 5,
+            page: 1,
+            search: null
+        };
+
+        scope.$watch("entityId", function(newVal, prevVal) {
+            if (newVal && !angular.equals(newVal, prevVal)) {
+                scope.resetFilter();
+                scope.getEntityAttributes(false, true);
+            }
+        });
+
+        scope.$watch("attributeScope", function(newVal, prevVal) {
+            if (newVal && !angular.equals(newVal, prevVal)) {
+                scope.mode = 'default';
+                scope.query.search = null;
+                scope.selectedAttributes = [];
+                scope.getEntityAttributes(false, true);
+            }
+        });
+
+        scope.resetFilter = function() {
+            scope.mode = 'default';
+            scope.query.search = null;
+            scope.selectedAttributes = [];
+            scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
+        }
+
+        scope.enterFilterMode = function() {
+            scope.query.search = '';
+        }
+
+        scope.exitFilterMode = function() {
+            scope.query.search = null;
+            scope.getEntityAttributes();
+        }
+
+        scope.$watch("query.search", function(newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal) && scope.query.search != null) {
+                scope.getEntityAttributes();
+            }
+        });
+
+        function success(attributes, update, apply) {
+            scope.attributes = attributes;
+            if (!update) {
+                scope.selectedAttributes = [];
+            }
+            if (apply) {
+                scope.$digest();
+            }
+        }
+
+        scope.onReorder = function() {
+            scope.getEntityAttributes(false, false);
+        }
+
+        scope.onPaginate = function() {
+            scope.getEntityAttributes(false, false);
+        }
+
+        scope.getEntityAttributes = function(forceUpdate, reset) {
+            if (scope.attributesDeferred) {
+                scope.attributesDeferred.resolve();
+            }
+            if (scope.entityId && scope.entityType && scope.attributeScope) {
+                if (reset) {
+                    scope.attributes = {
+                        count: 0,
+                        data: []
+                    };
+                }
+                scope.checkSubscription();
+                scope.attributesDeferred = attributeService.getEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value,
+                    scope.query, function(attributes, update, apply) {
+                        success(attributes, update || forceUpdate, apply);
+                    }
+                );
+            } else {
+                var deferred = $q.defer();
+                scope.attributesDeferred = deferred;
+                success({
+                    count: 0,
+                    data: []
+                });
+                deferred.resolve();
+            }
+        }
+
+        scope.checkSubscription = function() {
+            var newSubscriptionId = null;
+            if (scope.entityId && scope.entityType && scope.attributeScope.clientSide && scope.mode != 'widget') {
+                newSubscriptionId = attributeService.subscribeForEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value);
+            }
+            if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) {
+                attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
+            }
+            scope.subscriptionId = newSubscriptionId;
+        }
+
+        scope.$on('$destroy', function() {
+            if (scope.subscriptionId) {
+                attributeService.unsubscribeForEntityAttributes(scope.subscriptionId);
+            }
+        });
+
+        scope.editAttribute = function($event, attribute) {
+            if (!scope.attributeScope.clientSide) {
+                $event.stopPropagation();
+                $mdEditDialog.show({
+                    controller: EditAttributeValueController,
+                    templateUrl: editAttributeValueTemplate,
+                    locals: {attributeValue: attribute.value,
+                             save: function (model) {
+                                var updatedAttribute = angular.copy(attribute);
+                                updatedAttribute.value = model.value;
+                                attributeService.saveEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, [updatedAttribute]).then(
+                                    function success() {
+                                        scope.getEntityAttributes();
+                                    }
+                                );
+                            }},
+                    targetEvent: $event
+                });
+            }
+        }
+
+        scope.addAttribute = function($event) {
+            if (!scope.attributeScope.clientSide) {
+                $event.stopPropagation();
+                $mdDialog.show({
+                    controller: 'AddAttributeDialogController',
+                    controllerAs: 'vm',
+                    templateUrl: addAttributeDialogTemplate,
+                    parent: angular.element($document[0].body),
+                    locals: {entityType: scope.entityType, entityId: scope.entityId, attributeScope: scope.attributeScope.value},
+                    fullscreen: true,
+                    targetEvent: $event
+                }).then(function () {
+                    scope.getEntityAttributes();
+                });
+            }
+        }
+
+        scope.deleteAttributes = function($event) {
+            if (!scope.attributeScope.clientSide) {
+                $event.stopPropagation();
+                var confirm = $mdDialog.confirm()
+                    .targetEvent($event)
+                    .title($translate.instant('attribute.delete-attributes-title', {count: scope.selectedAttributes.length}, 'messageformat'))
+                    .htmlContent($translate.instant('attribute.delete-attributes-text'))
+                    .ariaLabel($translate.instant('attribute.delete-attributes'))
+                    .cancel($translate.instant('action.no'))
+                    .ok($translate.instant('action.yes'));
+                $mdDialog.show(confirm).then(function () {
+                        attributeService.deleteEntityAttributes(scope.entityType, scope.entityId, scope.attributeScope.value, scope.selectedAttributes).then(
+                            function success() {
+                                scope.selectedAttributes = [];
+                                scope.getEntityAttributes();
+                            }
+                        )
+                });
+            }
+        }
+
+        scope.nextWidget = function() {
+            if (scope.widgetsCarousel.index < scope.widgetsList.length-1) {
+                scope.widgetsCarousel.index++;
+            }
+        }
+
+        scope.prevWidget = function() {
+            if (scope.widgetsCarousel.index > 0) {
+                scope.widgetsCarousel.index--;
+            }
+        }
+
+        scope.enterWidgetMode = function() {
+
+            if (scope.widgetsIndexWatch) {
+                scope.widgetsIndexWatch();
+                scope.widgetsIndexWatch = null;
+            }
+
+            if (scope.widgetsBundleWatch) {
+                scope.widgetsBundleWatch();
+                scope.widgetsBundleWatch = null;
+            }
+
+            scope.mode = 'widget';
+            scope.checkSubscription();
+            scope.widgetsList = [];
+            scope.widgetsListCache = [];
+            scope.widgetsLoaded = false;
+            scope.widgetsCarousel = {
+                index: 0
+            }
+            scope.widgetsBundle = null;
+            scope.firstBundle = true;
+            scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
+
+            scope.aliasesInfo = {
+                entityAliases: {
+                    '1': {alias: scope.entityName, entityType: scope.entityType, entityId: scope.entityId}
+                },
+                entityAliasesInfo: {
+                    '1': [
+                        {name: scope.entityName, entityType: scope.entityType, id: scope.entityId}
+                    ]
+                }
+            };
+
+            var dataKeyType = scope.attributeScope === types.latestTelemetry ?
+                types.dataKeyType.timeseries : types.dataKeyType.attribute;
+
+            var datasource = {
+                type: types.datasourceType.entity,
+                entityAliasId: '1',
+                dataKeys: []
+            }
+            var i = 0;
+            for (var attr =0; attr < scope.selectedAttributes.length;attr++) {
+                var attribute = scope.selectedAttributes[attr];
+                var dataKey = {
+                    name: attribute.key,
+                    label: attribute.key,
+                    type: dataKeyType,
+                    color: utils.getMaterialColor(i),
+                    settings: {},
+                    _hash: Math.random()
+                }
+                datasource.dataKeys.push(dataKey);
+                i++;
+            }
+
+            scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function(newVal, prevVal) {
+                if (scope.mode === 'widget' && (newVal != prevVal)) {
+                    var index = scope.widgetsCarousel.index;
+                    for (var i = 0; i < scope.widgetsList.length; i++) {
+                        scope.widgetsList[i].splice(0, scope.widgetsList[i].length);
+                        if (i === index) {
+                            scope.widgetsList[i].push(scope.widgetsListCache[i][0]);
+                        }
+                    }
+                }
+            });
+
+            scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function(newVal, prevVal) {
+                if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) {
+                    scope.widgetsList = [];
+                    scope.widgetsListCache = [];
+                    scope.widgetsCarousel.index = 0;
+                    scope.firstBundle = false;
+                    if (scope.widgetsBundle) {
+                        scope.widgetsLoaded = false;
+                        var bundleAlias = scope.widgetsBundle.alias;
+                        var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid;
+                        widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
+                            function success(widgetTypes) {
+
+                                widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
+
+                                for (var i = 0; i < widgetTypes.length; i++) {
+                                    var widgetType = widgetTypes[i];
+                                    var widgetInfo = widgetService.toWidgetInfo(widgetType);
+                                    if (widgetInfo.type !== types.widgetType.static.value) {
+                                        var sizeX = widgetInfo.sizeX * 2;
+                                        var sizeY = widgetInfo.sizeY * 2;
+                                        var col = Math.floor(Math.max(0, (20 - sizeX) / 2));
+                                        var widget = {
+                                            isSystemType: isSystem,
+                                            bundleAlias: bundleAlias,
+                                            typeAlias: widgetInfo.alias,
+                                            type: widgetInfo.type,
+                                            title: widgetInfo.widgetName,
+                                            sizeX: sizeX,
+                                            sizeY: sizeY,
+                                            row: 0,
+                                            col: col,
+                                            config: angular.fromJson(widgetInfo.defaultConfig)
+                                        };
+
+                                        widget.config.title = widgetInfo.widgetName;
+                                        widget.config.datasources = [datasource];
+                                        var length;
+                                        if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) {
+                                            length = scope.widgetsListCache.push([widget]);
+                                            scope.widgetsList.push(length === 1 ? [widget] : []);
+                                        } else if (widgetInfo.type === types.widgetType.latest.value) {
+                                            length = scope.widgetsListCache.push([widget]);
+                                            scope.widgetsList.push(length === 1 ? [widget] : []);
+                                        }
+                                    }
+                                }
+                                scope.widgetsLoaded = true;
+                            }
+                        );
+                    }
+                }
+            });
+        }
+
+        scope.exitWidgetMode = function() {
+            if (scope.widgetsBundleWatch) {
+                scope.widgetsBundleWatch();
+                scope.widgetsBundleWatch = null;
+            }
+            if (scope.widgetsIndexWatch) {
+                scope.widgetsIndexWatch();
+                scope.widgetsIndexWatch = null;
+            }
+            scope.selectedWidgetsBundleAlias = null;
+            scope.mode = 'default';
+            scope.getEntityAttributes(true);
+        }
+
+        scope.getServerTimeDiff = function() {
+            return dashboardService.getServerTimeDiff();
+        }
+
+        scope.addWidgetToDashboard = function($event) {
+            if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
+                var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
+                $event.stopPropagation();
+                $mdDialog.show({
+                    controller: 'AddWidgetToDashboardDialogController',
+                    controllerAs: 'vm',
+                    templateUrl: addWidgetToDashboardDialogTemplate,
+                    parent: angular.element($document[0].body),
+                    locals: {entityId: scope.entityId, entityType: scope.entityType, entityName: scope.entityName, widget: angular.copy(widget)},
+                    fullscreen: true,
+                    targetEvent: $event
+                }).then(function () {
+
+                });
+            }
+        }
+
+        scope.loading = function() {
+            return $rootScope.loading;
+        }
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            entityId: '=',
+            entityName: '=',
+            disableAttributeScopeSelection: '@?'
+        }
+    };
+}
diff --git a/ui/src/app/entity/attribute/attribute-table.scss b/ui/src/app/entity/attribute/attribute-table.scss
new file mode 100644
index 0000000..3358230
--- /dev/null
+++ b/ui/src/app/entity/attribute/attribute-table.scss
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../../scss/constants';
+
+$md-light: rgba(255, 255, 255, 100%);
+$md-edit-icon-fill: #757575;
+
+md-toolbar.md-table-toolbar.alternate {
+  .md-toolbar-tools {
+      md-icon {
+        color: $md-light;
+      }
+  }
+}
+
+.md-table {
+  .md-cell {
+    ng-md-icon {
+      fill: $md-edit-icon-fill;
+      float: right;
+      height: 16px;
+    }
+  }
+}
+
+.widgets-carousel {
+  position: relative;
+  margin: 0px;
+
+  min-height: 150px !important;
+
+  tb-dashboard {
+    #gridster-parent {
+      padding: 0 7px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
new file mode 100644
index 0000000..bf3b8cd
--- /dev/null
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -0,0 +1,210 @@
+<!--
+
+    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" layout="column">
+    <section layout="row" ng-show="!disableAttributeScopeSelection">
+        <md-input-container class="md-block" style="width: 200px;">
+            <label translate>attribute.attributes-scope</label>
+            <md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
+                <md-option ng-repeat="scope in attributeScopes" ng-value="scope">
+                    {{scope.name | translate}}
+                </md-option>
+            </md-select>
+        </md-input-container>
+    </section>
+    <div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
+        <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
+                                                                 && !selectedAttributes.length
+                                                                 && query.search === null">
+            <div class="md-toolbar-tools">
+                <span translate>{{ attributeScope.name }}</span>
+                <span flex></span>
+                <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="addAttribute($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="enterFilterMode()">
+                    <md-icon>search</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.search' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="getEntityAttributes()">
+                    <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="mode==='default'
+                                                                 && !selectedAttributes.length
+                                                                 && 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 md-theme="tb-search-input" flex>
+                    <label>&nbsp;</label>
+                    <input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+                </md-input-container>
+                <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="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="mode==='default' && selectedAttributes.length">
+            <div class="md-toolbar-tools">
+                <span translate="{{attributeScope === types.latestTelemetry
+                                    ? 'attribute.selected-telemetry'
+                                    : 'attribute.selected-attributes'}}"
+                      translate-values="{count: selectedAttributes.length}"
+                      translate-interpolation="messageformat"></span>
+                <span flex></span>
+                <md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="deleteAttributes($event)">
+                    <md-icon>delete</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'action.delete' | translate }}
+                    </md-tooltip>
+                </md-button>
+                <md-button class="md-accent md-hue-2 md-raised" ng-click="enterWidgetMode()">
+                    <md-tooltip md-direction="top">
+                        {{ 'attribute.show-on-widget' | translate }}
+                    </md-tooltip>
+                    <md-icon>now_widgets</md-icon>
+                    <span translate>attribute.show-on-widget</span>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-toolbar class="md-table-toolbar alternate" ng-show="mode==='widget'">
+            <div class="md-toolbar-tools">
+                <div flex layout="row" layout-align="start">
+                    <span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
+                    <tb-widgets-bundle-select flex-offset="5"
+                                              flex
+                                              ng-model="widgetsBundle"
+                                              select-first-bundle="false"
+                                              select-bundle-alias="selectedWidgetsBundleAlias">
+                    </tb-widgets-bundle-select>
+                </div>
+                <md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)">
+                    <md-tooltip md-direction="top">
+                        {{ 'attribute.add-to-dashboard' | translate }}
+                    </md-tooltip>
+                    <md-icon>dashboard</md-icon>
+                    <span translate>attribute.add-to-dashboard</span>
+                </md-button>
+                <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitWidgetMode()">
+                    <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-table-container ng-show="mode!='widget'">
+            <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>
+                    </tr>
+                </thead>
+                <tbody md-body>
+                    <tr md-row md-select="attribute" md-select-id="key" md-auto-select ng-repeat="attribute in attributes.data">
+                        <td md-cell>{{attribute.lastUpdateTs | date :  'yyyy-MM-dd HH:mm:ss'}}</td>
+                        <td md-cell>{{attribute.key}}</td>
+                        <td md-cell ng-click="editAttribute($event, attribute)">
+                            <span>{{attribute.value}}</span>
+                            <span ng-show="!attributeScope.clientSide"><ng-md-icon size="16" icon="edit"></ng-md-icon></span>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+        </md-table-container>
+        <md-table-pagination ng-show="mode!='widget'" md-limit="query.limit" md-limit-options="[5, 10, 15]"
+                             md-page="query.page" md-total="{{attributes.count}}"
+                             md-on-paginate="onPaginate" md-page-select>
+        </md-table-pagination>
+        <ul flex rn-carousel ng-if="mode==='widget'" class="widgets-carousel"
+            rn-carousel-index="widgetsCarousel.index"
+            rn-carousel-buffered
+            rn-carousel-transition="fadeAndSlide"
+            rn-swipe-disabled="true">
+            <li ng-repeat="widgets in widgetsList">
+                <tb-dashboard
+                        aliases-info="aliasesInfo"
+                        widgets="widgets"
+                        get-st-diff="getServerTimeDiff()"
+                        columns="20"
+                        is-edit="false"
+                        is-mobile-disabled="true"
+                        is-edit-action-enabled="false"
+                        is-remove-action-enabled="false">
+                </tb-dashboard>
+            </li>
+            <span translate ng-if="widgetsLoaded &&
+                                   widgetsList.length === 0 &&
+                                   widgetsBundle"
+                  layout-align="center center"
+                  style="text-transform: uppercase; display: flex;"
+                  class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
+            <span translate ng-if="!widgetsBundle"
+                  layout-align="center center"
+                  style="text-transform: uppercase; display: flex;"
+                  class="md-headline tb-absolute-fill">widget.select-widgets-bundle</span>
+            <div ng-show="widgetsList.length > 1"
+                 style="position: absolute; left: 0; height: 100%;" layout="column" layout-align="center">
+                <md-button ng-show="widgetsCarousel.index > 0"
+                           class="md-icon-button"
+                           ng-click="prevWidget()">
+                    <md-icon>navigate_before</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'attribute.prev-widget' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+            <div ng-show="widgetsList.length > 1"
+                 style="position: absolute; right: 0; height: 100%;" layout="column" layout-align="center">
+                <md-button ng-show="widgetsCarousel.index < widgetsList.length-1"
+                           class="md-icon-button"
+                           ng-click="nextWidget()">
+                    <md-icon>navigate_next</md-icon>
+                    <md-tooltip md-direction="top">
+                        {{ 'attribute.next-widget' | translate }}
+                    </md-tooltip>
+                </md-button>
+            </div>
+            <div style="position: absolute; bottom: 0; width: 100%; font-size: 24px;" layout="row" layout-align="center">
+                <div rn-carousel-indicators
+                     ng-if="widgetsList.length > 1"
+                     slides="widgetsList"
+                     rn-carousel-index="widgetsCarousel.index">
+                </div>
+            </div>
+        </ul>
+    </div>
+</md-content>
diff --git a/ui/src/app/entity/attribute/edit-attribute-value.controller.js b/ui/src/app/entity/attribute/edit-attribute-value.controller.js
new file mode 100644
index 0000000..fba5b4d
--- /dev/null
+++ b/ui/src/app/entity/attribute/edit-attribute-value.controller.js
@@ -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.
+ */
+/*@ngInject*/
+export default function EditAttributeValueController($scope, $q, $element, types, attributeValue, save) {
+
+    $scope.valueTypes = types.valueType;
+
+    $scope.model = {};
+
+    $scope.model.value = attributeValue;
+
+    if ($scope.model.value === true || $scope.model.value === false) {
+        $scope.valueType = types.valueType.boolean;
+    } else if (angular.isNumber($scope.model.value)) {
+        if ($scope.model.value.toString().indexOf('.') == -1) {
+            $scope.valueType = types.valueType.integer;
+        } else {
+            $scope.valueType = types.valueType.double;
+        }
+    } else {
+        $scope.valueType = types.valueType.string;
+    }
+
+    $scope.submit = submit;
+    $scope.dismiss = dismiss;
+
+    function dismiss() {
+        $element.remove();
+    }
+
+    function update() {
+        if($scope.editDialog.$invalid) {
+            return $q.reject();
+        }
+
+        if(angular.isFunction(save)) {
+            return $q.when(save($scope.model));
+        }
+
+        return $q.resolve();
+    }
+
+    function submit() {
+        update().then(function () {
+            $scope.dismiss();
+        });
+    }
+
+    $scope.$watch('valueType', function(newVal, prevVal) {
+        if (newVal != prevVal) {
+            if ($scope.valueType === types.valueType.boolean) {
+                $scope.model.value = false;
+            } else {
+                $scope.model.value = null;
+            }
+        }
+    });
+}
diff --git a/ui/src/app/entity/attribute/edit-attribute-value.tpl.html b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html
new file mode 100644
index 0000000..8ce8d81
--- /dev/null
+++ b/ui/src/app/entity/attribute/edit-attribute-value.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+    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-edit-dialog>
+    <form name="editDialog" ng-submit="submit()">
+        <div layout="column" class="md-content" style="width: 400px;">
+            <fieldset>
+                <section layout="row">
+                    <md-input-container flex="40" class="md-block">
+                        <label translate>value.type</label>
+                        <md-select ng-model="valueType">
+                            <md-option ng-repeat="type in valueTypes" ng-value="type">
+                                <md-icon md-svg-icon="{{ type.icon }}"></md-icon>
+                                <span>{{type.name | translate}}</span>
+                            </md-option>
+                        </md-select>
+                    </md-input-container>
+                    <md-input-container ng-if="valueType===valueTypes.string" flex="60" class="md-block">
+                        <label translate>value.string-value</label>
+                        <input required name="value" ng-model="model.value">
+                        <div ng-messages="editDialog.value.$error">
+                            <div translate ng-message="required">attribute.value-required</div>
+                        </div>
+                    </md-input-container>
+                    <md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block">
+                        <label translate>value.integer-value</label>
+                        <input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="model.value">
+                        <div ng-messages="editDialog.value.$error">
+                            <div translate ng-message="required">attribute.value-required</div>
+                            <div translate ng-message="pattern">value.invalid-integer-value</div>
+                        </div>
+                    </md-input-container>
+                    <md-input-container ng-if="valueType===valueTypes.double" flex="60" class="md-block">
+                        <label translate>value.double-value</label>
+                        <input required name="value" type="number" step="any" ng-model="model.value">
+                        <div ng-messages="editDialog.value.$error">
+                            <div translate ng-message="required">attribute.value-required</div>
+                        </div>
+                    </md-input-container>
+                    <div layout="column" layout-align="center" flex="60" ng-if="valueType===valueTypes.boolean">
+                        <md-checkbox ng-model="model.value" style="margin-bottom: 0px;">
+                            {{ (model.value ? 'value.true' : 'value.false') | translate }}
+                        </md-checkbox>
+                    </div>
+                </section>
+            </fieldset>
+        </div>
+        <div layout="row" layout-align="end" class="md-actions">
+            <md-button ng-click="dismiss()">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+            <md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.update' | translate }}
+            </md-button>
+        </div>
+    </form>
+</md-edit-dialog>
\ No newline at end of file
diff --git a/ui/src/app/entity/entity-aliases.controller.js b/ui/src/app/entity/entity-aliases.controller.js
new file mode 100644
index 0000000..4eb1737
--- /dev/null
+++ b/ui/src/app/entity/entity-aliases.controller.js
@@ -0,0 +1,221 @@
+/*
+ * 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-aliases.scss';
+
+/*@ngInject*/
+export default function EntityAliasesController(utils, entityService, toast, $scope, $mdDialog, $document, $q, $translate,
+                                                  types, config) {
+
+    var vm = this;
+
+    vm.isSingleEntityAlias = config.isSingleEntityAlias;
+    vm.singleEntityAlias = config.singleEntityAlias;
+    vm.entityAliases = [];
+    vm.title = config.customTitle ? config.customTitle : 'entity.aliases';
+    vm.disableAdd = config.disableAdd;
+    vm.aliasToWidgetsMap = {};
+    vm.allowedEntityTypes = config.allowedEntityTypes;
+
+    vm.onFilterEntityChanged = onFilterEntityChanged;
+    vm.addAlias = addAlias;
+    vm.removeAlias = removeAlias;
+
+    vm.cancel = cancel;
+    vm.save = save;
+
+    initController();
+
+    function initController() {
+        var aliasId;
+        if (config.widgets) {
+            var widgetsTitleList, widget;
+            if (config.isSingleWidget && config.widgets.length == 1) {
+                widget = config.widgets[0];
+                widgetsTitleList = [widget.config.title];
+                for (aliasId in config.entityAliases) {
+                    vm.aliasToWidgetsMap[aliasId] = widgetsTitleList;
+                }
+            } else {
+                for (var w in config.widgets) {
+                    widget = config.widgets[w];
+                    if (widget.type === types.widgetType.rpc.value) {
+                        if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
+                            var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
+                            widgetsTitleList = vm.aliasToWidgetsMap[targetDeviceAliasId];
+                            if (!widgetsTitleList) {
+                                widgetsTitleList = [];
+                                vm.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
+                            }
+                            widgetsTitleList.push(widget.config.title);
+                        }
+                    } else {
+                        var datasources = utils.validateDatasources(widget.config.datasources);
+                        for (var i=0;i<datasources.length;i++) {
+                            var datasource = datasources[i];
+                            if (datasource.type === types.datasourceType.entity && datasource.entityAliasId) {
+                                widgetsTitleList = vm.aliasToWidgetsMap[datasource.entityAliasId];
+                                if (!widgetsTitleList) {
+                                    widgetsTitleList = [];
+                                    vm.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList;
+                                }
+                                widgetsTitleList.push(widget.config.title);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (vm.isSingleEntityAlias) {
+            checkEntityAlias(vm.singleEntityAlias);
+        }
+
+        for (aliasId in config.entityAliases) {
+            var entityAlias = config.entityAliases[aliasId];
+            var result = {id: aliasId, alias: entityAlias.alias, entityType: entityAlias.entityType, entityFilter: entityAlias.entityFilter, changed: true};
+            checkEntityAlias(result);
+            vm.entityAliases.push(result);
+        }
+    }
+
+    function checkEntityAlias(entityAlias) {
+        if (!entityAlias.entityType) {
+            entityAlias.entityType = types.entityType.device;
+        }
+        if (!entityAlias.entityFilter || entityAlias.entityFilter == null) {
+            entityAlias.entityFilter = {
+                useFilter: false,
+                entityNameFilter: '',
+                entityList: [],
+            };
+        }
+    }
+
+    function onFilterEntityChanged(entity, entityAlias) {
+        if (entityAlias) {
+            if (!entityAlias.alias || entityAlias.alias.length == 0) {
+                entityAlias.changed = false;
+            }
+            if (!entityAlias.changed && entity && entityAlias.entityType) {
+                entityAlias.alias = entityService.entityName(entityAlias.entityType, entity);
+            }
+        }
+    }
+
+    function addAlias() {
+        var aliasId = 0;
+        for (var a in vm.entityAliases) {
+            aliasId = Math.max(vm.entityAliases[a].id, aliasId);
+        }
+        aliasId++;
+        var entityAlias = {id: aliasId, alias: '', entityType: types.entityType.device,
+            entityFilter: {useFilter: false, entityNameFilter: '', entityList: []}, changed: false};
+        vm.entityAliases.push(entityAlias);
+    }
+
+    function removeAlias($event, entityAlias) {
+        var index = vm.entityAliases.indexOf(entityAlias);
+        if (index > -1) {
+            var widgetsTitleList = vm.aliasToWidgetsMap[entityAlias.id];
+            if (widgetsTitleList) {
+                var widgetsListHtml = '';
+                for (var t in widgetsTitleList) {
+                    widgetsListHtml += '<br/>\'' + widgetsTitleList[t] + '\'';
+                }
+                var alert = $mdDialog.alert()
+                    .parent(angular.element($document[0].body))
+                    .clickOutsideToClose(true)
+                    .title($translate.instant('entity.unable-delete-entity-alias-title'))
+                    .htmlContent($translate.instant('entity.unable-delete-entity-alias-text', {entityAlias: entityAlias.alias, widgetsList: widgetsListHtml}))
+                    .ariaLabel($translate.instant('entity.unable-delete-entity-alias-title'))
+                    .ok($translate.instant('action.close'))
+                    .targetEvent($event);
+                alert._options.skipHide = true;
+                alert._options.fullscreen = true;
+
+                $mdDialog.show(alert);
+            } else {
+                vm.entityAliases.splice(index, 1);
+                if ($scope.theForm) {
+                    $scope.theForm.$setDirty();
+                }
+            }
+        }
+    }
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function cleanupEntityFilter(entityFilter) {
+        if (entityFilter.useFilter) {
+            entityFilter.entityList = [];
+        } else {
+            entityFilter.entityNameFilter = '';
+        }
+        return entityFilter;
+    }
+
+    function save() {
+
+        var entityAliases = {};
+        var uniqueAliasList = {};
+
+        var valid = true;
+        var aliasId, maxAliasId;
+        var alias;
+        var i;
+
+        if (vm.isSingleEntityAlias) {
+            maxAliasId = 0;
+            vm.singleEntityAlias.entityFilter = cleanupEntityFilter(vm.singleEntityAlias.entityFilter);
+            for (i = 0; i < vm.entityAliases.length; i ++) {
+                aliasId = vm.entityAliases[i].id;
+                alias = vm.entityAliases[i].alias;
+                if (alias === vm.singleEntityAlias.alias) {
+                    valid = false;
+                    break;
+                }
+                maxAliasId = Math.max(aliasId, maxAliasId);
+            }
+            maxAliasId++;
+            vm.singleEntityAlias.id = maxAliasId;
+        } else {
+            for (i = 0; i < vm.entityAliases.length; i++) {
+                aliasId = vm.entityAliases[i].id;
+                alias = vm.entityAliases[i].alias;
+                if (!uniqueAliasList[alias]) {
+                    uniqueAliasList[alias] = alias;
+                    entityAliases[aliasId] = {alias: alias, entityType: vm.entityAliases[i].entityType, entityFilter: cleanupEntityFilter(vm.entityAliases[i].entityFilter)};
+                } else {
+                    valid = false;
+                    break;
+                }
+            }
+        }
+        if (valid) {
+            $scope.theForm.$setPristine();
+            if (vm.isSingleEntityAlias) {
+                $mdDialog.hide(vm.singleEntityAlias);
+            } else {
+                $mdDialog.hide(entityAliases);
+            }
+        } else {
+            toast.showError($translate.instant('entity.duplicate-alias-error', {alias: alias}));
+        }
+    }
+
+}
diff --git a/ui/src/app/entity/entity-aliases.scss b/ui/src/app/entity/entity-aliases.scss
new file mode 100644
index 0000000..b108dc0
--- /dev/null
+++ b/ui/src/app/entity/entity-aliases.scss
@@ -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.
+ */
+
+.tb-aliases-dialog {
+  .md-dialog-content {
+    padding-bottom: 0px;
+  }
+  .tb-alias {
+    padding: 10px 0 0 10px;
+    margin: 5px;
+    md-select.tb-entity-type-select {
+      padding-bottom: 24px;
+    }
+  }
+}
diff --git a/ui/src/app/entity/entity-aliases.tpl.html b/ui/src/app/entity/entity-aliases.tpl.html
new file mode 100644
index 0000000..bc9e269
--- /dev/null
+++ b/ui/src/app/entity/entity-aliases.tpl.html
@@ -0,0 +1,106 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<md-dialog class="tb-aliases-dialog" style="width: 700px;" aria-label="{{ vm.title | translate }}">
+	<form name="theForm" ng-submit="vm.save()">
+		<md-toolbar>
+			<div class="md-toolbar-tools">
+				<h2>{{ vm.isSingleEntityAlias ? ('entity.configure-alias' | translate:vm.singleEntityAlias ) : (vm.title | translate) }}</h2>
+				<span flex></span>
+				<md-button class="md-icon-button" ng-click="vm.cancel()">
+					<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+				</md-button>
+			</div>
+		</md-toolbar>
+		<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+		<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+		<md-dialog-content>
+			<div class="md-dialog-content">
+				<fieldset ng-disabled="loading">
+					<div ng-show="vm.isSingleEntityAlias" layout="row">
+						<tb-entity-type-select style="min-width: 100px;"
+											   ng-model="vm.singleEntityAlias.entityType"
+											   allowed-entity-types="vm.allowedEntityTypes">
+						</tb-entity-type-select>
+						<tb-entity-filter flex entity-type="vm.singleEntityAlias.entityType" ng-model="vm.singleEntityAlias.entityFilter">
+						</tb-entity-filter>
+					</div>
+					<div ng-show="!vm.isSingleEntityAlias" flex layout="row" layout-align="start center">
+						<span flex="5"></span>
+						<div flex layout="row" layout-align="start center"
+							 style="padding: 0 0 0 10px; margin: 5px;">
+							<span translate flex="20" style="min-width: 100px;">entity.alias</span>
+							<span translate flex="20" style="min-width: 100px;">entity.type</span>
+							<span translate flex="60" style="min-width: 190px; padding-left: 10px;">entity.entities</span>
+							<span style="min-width: 40px;"></span>
+						</div>
+					</div>
+					<div ng-show="!vm.isSingleEntityAlias" style="max-height: 500px; overflow: auto; padding-bottom: 20px;">
+						<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="entityAlias in vm.entityAliases track by $index">
+							<span flex="5">{{$index + 1}}.</span>
+							<div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
+								<md-input-container flex="20" style="min-width: 100px;" md-no-float class="md-block">
+									<input required ng-change="entityAlias.changed=true" name="alias" placeholder="{{ 'entity.alias' | translate }}" ng-model="entityAlias.alias">
+									<div ng-messages="aliasForm.alias.$error">
+										<div translate ng-message="required">entity.alias-required</div>
+									</div>
+								</md-input-container>
+								<section flex="20" layout="column" style="min-width: 100px;" >
+									<tb-entity-type-select hide-label style="padding-left: 10px;"
+													  ng-model="entityAlias.entityType"
+													  allowed-entity-types="vm.allowedEntityTypes">
+									</tb-entity-type-select>
+								</section>
+								<section flex="60" layout="column">
+									<tb-entity-filter style="padding-left: 10px;"
+													  entity-type="entityAlias.entityType"
+													  ng-model="entityAlias.entityFilter"
+													  on-matching-entity-change="vm.onFilterEntityChanged(entity, entityAlias)">
+									</tb-entity-filter>
+								</section>
+								<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
+										   ng-click="vm.removeAlias($event, entityAlias)" aria-label="{{ 'action.remove' | translate }}">
+									<md-tooltip md-direction="top">
+										{{ 'entity.remove-alias' | translate }}
+									</md-tooltip>
+									<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
+										close
+									</md-icon>
+								</md-button>
+							</div>
+						</div>
+					</div>
+					<div ng-show="!vm.isSingleEntityAlias && !vm.disableAdd" style="padding-bottom: 10px;">
+						<md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
+							<md-tooltip md-direction="top">
+								{{ 'entity.add-alias' | translate }}
+							</md-tooltip>
+							<span translate>action.add</span>
+						</md-button>
+					</div>
+				</fieldset>
+			</div>
+		</md-dialog-content>
+		<md-dialog-actions layout="row">
+			<span flex></span>
+			<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
+				{{ 'action.save' | translate }}
+			</md-button>
+			<md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+		</md-dialog-actions>
+	</form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js
new file mode 100644
index 0000000..777dbb8
--- /dev/null
+++ b/ui/src/app/entity/entity-filter.directive.js
@@ -0,0 +1,234 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entityFilterTemplate from './entity-filter.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import './entity-filter.scss';
+
+/*@ngInject*/
+export default function EntityFilterDirective($compile, $templateCache, $q, entityService) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+
+        var template = $templateCache.get(entityFilterTemplate);
+        element.html(template);
+
+        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) {
+                if (result) {
+                    deferred.resolve(result);
+                } else {
+                    deferred.resolve([]);
+                }
+            }, function fail() {
+                deferred.reject();
+            });
+            return deferred.promise;
+        }
+
+        scope.updateValidity = function() {
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                var valid;
+                if (value.useFilter) {
+                    ngModelCtrl.$setValidity('entityList', true);
+                    if (angular.isDefined(value.entityNameFilter) && value.entityNameFilter.length > 0) {
+                        ngModelCtrl.$setValidity('entityNameFilter', true);
+                        valid = angular.isDefined(scope.model.matchingFilterEntity) && scope.model.matchingFilterEntity != null;
+                        ngModelCtrl.$setValidity('entityNameFilterEntityMatch', valid);
+                    } else {
+                        ngModelCtrl.$setValidity('entityNameFilter', false);
+                    }
+                } else {
+                    ngModelCtrl.$setValidity('entityNameFilter', true);
+                    ngModelCtrl.$setValidity('entityNameFilterDeviceMatch', true);
+                    valid = angular.isDefined(value.entityList) && value.entityList.length > 0;
+                    ngModelCtrl.$setValidity('entityList', valid);
+                }
+            }
+        }
+
+        ngModelCtrl.$render = function () {
+            destroyWatchers();
+            scope.model = {
+                useFilter: false,
+                entityList: [],
+                entityNameFilter: ''
+            }
+            if (ngModelCtrl.$viewValue) {
+                var value = ngModelCtrl.$viewValue;
+                var model = scope.model;
+                model.useFilter = value.useFilter === true ? true: false;
+                model.entityList = [];
+                model.entityNameFilter = value.entityNameFilter || '';
+                processEntityNameFilter(model.entityNameFilter).then(
+                    function(entity) {
+                        scope.model.matchingFilterEntity = entity;
+                        if (value.entityList && value.entityList.length > 0) {
+                            entityService.getEntities(scope.entityType, value.entityList).then(function (entities) {
+                                model.entityList = entities;
+                                updateMatchingEntity();
+                                initWatchers();
+                            });
+                        } else {
+                            updateMatchingEntity();
+                            initWatchers();
+                        }
+                    }
+                )
+            }
+        }
+
+        function updateMatchingEntity() {
+            if (scope.model.useFilter) {
+                scope.model.matchingEntity = scope.model.matchingFilterEntity;
+            } else {
+                if (scope.model.entityList && scope.model.entityList.length > 0) {
+                    scope.model.matchingEntity = scope.model.entityList[0];
+                } else {
+                    scope.model.matchingEntity = null;
+                }
+            }
+        }
+
+        function processEntityNameFilter(entityNameFilter) {
+            var deferred = $q.defer();
+            if (angular.isDefined(entityNameFilter) && entityNameFilter.length > 0) {
+                scope.fetchEntities(entityNameFilter, 1).then(function (entities) {
+                    if (entities && entities.length > 0) {
+                        deferred.resolve(entities[0]);
+                    } else {
+                        deferred.resolve(null);
+                    }
+                });
+            } else {
+                deferred.resolve(null);
+            }
+            return deferred.promise;
+        }
+
+        function destroyWatchers() {
+            if (scope.entityTypeDeregistration) {
+                scope.entityTypeDeregistration();
+                scope.entityTypeDeregistration = null;
+            }
+            if (scope.entityListDeregistration) {
+                scope.entityListDeregistration();
+                scope.entityListDeregistration = null;
+            }
+            if (scope.useFilterDeregistration) {
+                scope.useFilterDeregistration();
+                scope.useFilterDeregistration = null;
+            }
+            if (scope.entityNameFilterDeregistration) {
+                scope.entityNameFilterDeregistration();
+                scope.entityNameFilterDeregistration = null;
+            }
+            if (scope.matchingEntityDeregistration) {
+                scope.matchingEntityDeregistration();
+                scope.matchingEntityDeregistration = null;
+            }
+        }
+
+        function initWatchers() {
+
+            scope.entityTypeDeregistration = scope.$watch('entityType', function (newEntityType, prevEntityType) {
+                if (!angular.equals(newEntityType, prevEntityType)) {
+                    scope.model.entityList = [];
+                    scope.model.entityNameFilter = '';
+                }
+            });
+
+            scope.entityListDeregistration = scope.$watch('model.entityList', function () {
+                if (ngModelCtrl.$viewValue) {
+                    var value = ngModelCtrl.$viewValue;
+                    value.entityList = [];
+                    if (scope.model.entityList && scope.model.entityList.length > 0) {
+                        for (var i=0;i<scope.model.entityList.length;i++) {
+                            value.entityList.push(scope.model.entityList[i].id.id);
+                        }
+                    }
+                    updateMatchingEntity();
+                    ngModelCtrl.$setViewValue(value);
+                    scope.updateValidity();
+                }
+            }, true);
+            scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
+                if (ngModelCtrl.$viewValue) {
+                    var value = ngModelCtrl.$viewValue;
+                    value.useFilter = scope.model.useFilter;
+                    updateMatchingEntity();
+                    ngModelCtrl.$setViewValue(value);
+                    scope.updateValidity();
+                }
+            });
+            scope.entityNameFilterDeregistration = scope.$watch('model.entityNameFilter', function (newNameFilter, prevNameFilter) {
+                if (ngModelCtrl.$viewValue) {
+                    if (!angular.equals(newNameFilter, prevNameFilter)) {
+                        var value = ngModelCtrl.$viewValue;
+                        value.entityNameFilter = scope.model.entityNameFilter;
+                        processEntityNameFilter(value.entityNameFilter).then(
+                            function(entity) {
+                                scope.model.matchingFilterEntity = entity;
+                                updateMatchingEntity();
+                                ngModelCtrl.$setViewValue(value);
+                                scope.updateValidity();
+                            }
+                        );
+                    }
+                }
+            });
+
+            scope.matchingEntityDeregistration = scope.$watch('model.matchingEntity', function (newMatchingEntity, prevMatchingEntity) {
+                if (!angular.equals(newMatchingEntity, prevMatchingEntity)) {
+                    if (scope.onMatchingEntityChange) {
+                        scope.onMatchingEntityChange({entity: newMatchingEntity});
+                    }
+                }
+            });
+        }
+
+        $compile(element.contents())(scope);
+
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            entityType: '=',
+            isEdit: '=',
+            onMatchingEntityChange: '&'
+        }
+    };
+
+}
diff --git a/ui/src/app/entity/entity-filter.scss b/ui/src/app/entity/entity-filter.scss
new file mode 100644
index 0000000..1525842
--- /dev/null
+++ b/ui/src/app/entity/entity-filter.scss
@@ -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.
+ */
+.tb-entity-filter {
+  #entity_list_chips {
+    .md-chips {
+      padding-bottom: 1px;
+    }
+  }
+  .entity-name-filter-input {
+    margin-top: 10px;
+    margin-bottom: 0px;
+    .md-errors-spacer {
+      min-height: 0px;
+    }
+  }
+  .tb-filter-switch {
+    padding-left: 10px;
+    .filter-switch {
+      margin: 0;
+    }
+    .filter-label {
+      margin: 5px 0;
+    }
+  }
+  .tb-error-messages {
+    margin-top: -11px;
+    height: 35px;
+    .tb-error-message {
+      padding-left: 1px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html
new file mode 100644
index 0000000..508e4ba
--- /dev/null
+++ b/ui/src/app/entity/entity-filter.tpl.html
@@ -0,0 +1,67 @@
+<!--
+
+    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.
+
+-->
+<section layout='column' class="tb-entity-filter">
+    <section layout='row'>
+        <section layout="column" flex ng-show="!model.useFilter">
+            <md-chips flex
+                      id="entity_list_chips"
+                      ng-required="!useFilter"
+                      ng-model="model.entityList" md-autocomplete-snap
+                      md-require-match="true">
+                <md-autocomplete
+                        md-no-cache="true"
+                        id="entity"
+                        md-selected-item="selectedEntity"
+                        md-search-text="entitySearchText"
+                        md-items="item in fetchEntities(entitySearchText, 10)"
+                        md-item-text="item.name"
+                        md-min-length="0"
+                        placeholder="{{ 'entity.entity-list' | translate }}">
+                        <md-item-template>
+                            <span md-highlight-text="entitySearchText" md-highlight-flags="^i">{{itemName(item)}}</span>
+                        </md-item-template>
+                        <md-not-found>
+                            <span translate translate-values='{ entity: entitySearchText }'>entity.no-entities-matching</span>
+                        </md-not-found>
+                </md-autocomplete>
+                <md-chip-template>
+                    <span>
+                      <strong>{{itemName($chip)}}</strong>
+                    </span>
+                </md-chip-template>
+            </md-chips>
+        </section>
+        <section layout="row" flex ng-show="model.useFilter">
+            <md-input-container flex class="entity-name-filter-input">
+                <label translate>entity.name-starts-with</label>
+                <input ng-model="model.entityNameFilter" aria-label="{{ 'entity.name-starts-with' | translate }}">
+            </md-input-container>
+        </section>
+        <section class="tb-filter-switch" layout="column" layout-align="center center">
+            <label class="tb-small filter-label" translate>entity.use-entity-name-filter</label>
+            <md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
+            </md-switch>
+        </section>
+    </section>
+    <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
+        <div translate ng-message="entityList" class="tb-error-message">entity.entity-list-empty</div>
+        <div translate ng-message="entityNameFilter" class="tb-error-message">entity.entity-name-filter-required</div>
+        <div translate translate-values='{ entity: model.entityNameFilter }' ng-message="entityNameFilterEntityMatch"
+             class="tb-error-message">entity.entity-name-filter-no-entity-matched</div>
+    </div>
+</section>
\ No newline at end of file
diff --git a/ui/src/app/entity/entity-type-select.directive.js b/ui/src/app/entity/entity-type-select.directive.js
new file mode 100644
index 0000000..6a5a045
--- /dev/null
+++ b/ui/src/app/entity/entity-type-select.directive.js
@@ -0,0 +1,120 @@
+/*
+ * 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-type-select.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entityTypeSelectTemplate from './entity-type-select.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function EntityTypeSelect($compile, $templateCache, userService, types) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(entityTypeSelectTemplate);
+        element.html(template);
+
+        if (angular.isDefined(attrs.hideLabel)) {
+            scope.showLabel = false;
+        } else {
+            scope.showLabel = true;
+        }
+
+        scope.ngModelCtrl = ngModelCtrl;
+
+        var authority = userService.getAuthority();
+        scope.entityTypes = {};
+        switch(authority) {
+            case 'SYS_ADMIN':
+                scope.entityTypes.tenant = types.entityType.tenant;
+                scope.entityTypes.rule = types.entityType.rule;
+                scope.entityTypes.plugin = types.entityType.plugin;
+                break;
+            case 'TENANT_ADMIN':
+                scope.entityTypes.device = types.entityType.device;
+                scope.entityTypes.asset = types.entityType.asset;
+                scope.entityTypes.customer = types.entityType.customer;
+                scope.entityTypes.rule = types.entityType.rule;
+                scope.entityTypes.plugin = types.entityType.plugin;
+                break;
+            case 'CUSTOMER_USER':
+                scope.entityTypes.device = types.entityType.device;
+                scope.entityTypes.asset = types.entityType.asset;
+                break;
+        }
+
+        if (scope.allowedEntityTypes) {
+            for (var entityType in scope.entityTypes) {
+                if (scope.allowedEntityTypes.indexOf(scope.entityTypes[entityType]) === -1) {
+                    delete scope.entityTypes[entityType];
+                }
+            }
+        }
+
+        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';
+            }
+        }
+
+        scope.updateValidity = function () {
+            var value = ngModelCtrl.$viewValue;
+            var valid = angular.isDefined(value) && value != null;
+            ngModelCtrl.$setValidity('entityType', valid);
+        };
+
+        scope.$watch('entityType', function (newValue, prevValue) {
+            if (!angular.equals(newValue, prevValue)) {
+                scope.updateView();
+            }
+        });
+
+        scope.updateView = function () {
+            ngModelCtrl.$setViewValue(scope.entityType);
+            scope.updateValidity();
+        };
+
+        ngModelCtrl.$render = function () {
+            if (ngModelCtrl.$viewValue) {
+                scope.entityType = ngModelCtrl.$viewValue;
+            }
+        };
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        link: linker,
+        scope: {
+            allowedEntityTypes: "=?"
+        }
+    };
+}
diff --git a/ui/src/app/entity/entity-type-select.scss b/ui/src/app/entity/entity-type-select.scss
new file mode 100644
index 0000000..58ff6cc
--- /dev/null
+++ b/ui/src/app/entity/entity-type-select.scss
@@ -0,0 +1,18 @@
+/**
+ * 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-select.tb-entity-type-select {
+}
diff --git a/ui/src/app/entity/entity-type-select.tpl.html b/ui/src/app/entity/entity-type-select.tpl.html
new file mode 100644
index 0000000..e31cbf5
--- /dev/null
+++ b/ui/src/app/entity/entity-type-select.tpl.html
@@ -0,0 +1,25 @@
+<!--
+
+    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-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-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
diff --git a/ui/src/app/entity/index.js b/ui/src/app/entity/index.js
new file mode 100644
index 0000000..07c0862
--- /dev/null
+++ b/ui/src/app/entity/index.js
@@ -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.
+ */
+
+import EntityAliasesController from './entity-aliases.controller';
+import EntityTypeSelectDirective from './entity-type-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';
+
+export default angular.module('thingsboard.entity', [])
+    .controller('EntityAliasesController', EntityAliasesController)
+    .controller('AliasesEntitySelectPanelController', AliasesEntitySelectPanelController)
+    .controller('AddAttributeDialogController', AddAttributeDialogController)
+    .controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
+    .directive('tbEntityTypeSelect', EntityTypeSelectDirective)
+    .directive('tbEntityFilter', EntityFilterDirective)
+    .directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
+    .directive('tbAttributeTable', AttributeTableDirective)
+    .name;
diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js
index 62d6c2d..22f9d75 100644
--- a/ui/src/app/help/help-links.constant.js
+++ b/ui/src/app/help/help-links.constant.js
@@ -78,6 +78,7 @@ export default angular.module('thingsboard.help', [])
                 pluginActionRestApiCall: helpBaseUrl + "/docs/reference/actions/rest-api-call-plugin-action",
                 tenants: helpBaseUrl + "/docs/user-guide/ui/tenants",
                 customers: helpBaseUrl + "/docs/user-guide/ui/customers",
+                assets: helpBaseUrl + "/docs/user-guide/ui/assets",
                 devices: helpBaseUrl + "/docs/user-guide/ui/devices",
                 dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards",
                 users: helpBaseUrl + "/docs/user-guide/ui/users",
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 0c04291..4b89a99 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -16,7 +16,7 @@
 /* eslint-disable import/no-unresolved, import/default */
 
 import importDialogTemplate from './import-dialog.tpl.html';
-import deviceAliasesTemplate from '../dashboard/device-aliases.tpl.html';
+import entityAliasesTemplate from '../entity/entity-aliases.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
@@ -24,8 +24,8 @@ import deviceAliasesTemplate from '../dashboard/device-aliases.tpl.html';
 /* eslint-disable no-undef, angular/window-service, angular/document-service */
 
 /*@ngInject*/
-export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, types,
-                                     deviceService, dashboardService, pluginService, ruleService, widgetService, toast) {
+export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, types, dashboardUtils,
+                                     entityService, dashboardService, pluginService, ruleService, widgetService, toast) {
 
 
     var service = {
@@ -339,21 +339,49 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         exportToPc(prepareExport(widgetItem), name + '.json');
     }
 
-    function prepareDeviceAlias(aliasInfo) {
-        var deviceFilter;
+    function prepareAliasesInfo(aliasesInfo) {
+        var datasourceAliases = aliasesInfo.datasourceAliases;
+        var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
+        var datasourceIndex;
+        if (datasourceAliases || targetDeviceAliases) {
+            if (datasourceAliases) {
+                for (datasourceIndex in datasourceAliases) {
+                    datasourceAliases[datasourceIndex] = prepareEntityAlias(datasourceAliases[datasourceIndex]);
+                }
+            }
+            if (targetDeviceAliases) {
+                for (datasourceIndex in targetDeviceAliases) {
+                    targetDeviceAliases[datasourceIndex] = prepareEntityAlias(targetDeviceAliases[datasourceIndex]);
+                }
+            }
+        }
+    }
+
+    function prepareEntityAlias(aliasInfo) {
+        var entityFilter;
+        var entityType;
         if (aliasInfo.deviceId) {
-            deviceFilter = {
+            entityFilter = {
                 useFilter: false,
-                deviceNameFilter: '',
-                deviceList: [aliasInfo.deviceId]
+                entityNameFilter: '',
+                entityList: [aliasInfo.deviceId]
+            }
+            entityType = types.entityType.device;
+        } else if (aliasInfo.deviceFilter) {
+            entityFilter = {
+                useFilter: aliasInfo.deviceFilter.useFilter,
+                entityNameFilter: aliasInfo.deviceFilter.deviceNameFilter,
+                entityList: aliasInfo.deviceFilter.deviceList
             }
-            delete aliasInfo.deviceId;
+            entityType = types.entityType.device;
         } else {
-            deviceFilter = aliasInfo.deviceFilter;
+            entityFilter = aliasInfo.entityFilter;
+            entityType = aliasInfo.entityType;
         }
         return {
             alias: aliasInfo.aliasName,
-            deviceFilter: deviceFilter
+            entityType: entityType,
+            entityFilter: entityFilter
         };
     }
 
@@ -364,13 +392,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                     toast.showError($translate.instant('dashboard.invalid-widget-file-error'));
                 } else {
                     var widget = widgetItem.widget;
-                    var aliasesInfo = widgetItem.aliasesInfo;
+                    var aliasesInfo = prepareAliasesInfo(widgetItem.aliasesInfo);
                     var originalColumns = widgetItem.originalColumns;
 
                     var datasourceAliases = aliasesInfo.datasourceAliases;
                     var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
                     if (datasourceAliases || targetDeviceAliases) {
-                        var deviceAliases = {};
+                        var entityAliases = {};
                         var datasourceAliasesMap = {};
                         var targetDeviceAliasesMap = {};
                         var aliasId = 1;
@@ -378,35 +406,37 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                         if (datasourceAliases) {
                             for (datasourceIndex in datasourceAliases) {
                                 datasourceAliasesMap[aliasId] = datasourceIndex;
-                                deviceAliases[aliasId] = prepareDeviceAlias(datasourceAliases[datasourceIndex]);
+                                entityAliases[aliasId] = datasourceAliases[datasourceIndex];
                                 aliasId++;
                             }
                         }
                         if (targetDeviceAliases) {
                             for (datasourceIndex in targetDeviceAliases) {
                                 targetDeviceAliasesMap[aliasId] = datasourceIndex;
-                                deviceAliases[aliasId] = prepareDeviceAlias(targetDeviceAliases[datasourceIndex]);
+                                entityAliases[aliasId] = targetDeviceAliases[datasourceIndex];
                                 aliasId++;
                             }
                         }
 
-                        var aliasIds = Object.keys(deviceAliases);
+                        var aliasIds = Object.keys(entityAliases);
                         if (aliasIds.length > 0) {
-                            processDeviceAliases(deviceAliases, aliasIds).then(
-                                function(missingDeviceAliases) {
-                                    if (Object.keys(missingDeviceAliases).length > 0) {
+                            processEntityAliases(entityAliases, aliasIds).then(
+                                function(missingEntityAliases) {
+                                    if (Object.keys(missingEntityAliases).length > 0) {
                                         editMissingAliases($event, [ widget ],
-                                              true, 'dashboard.widget-import-missing-aliases-title', missingDeviceAliases).then(
-                                            function success(updatedDeviceAliases) {
-                                                for (var aliasId in updatedDeviceAliases) {
-                                                    var deviceAlias = updatedDeviceAliases[aliasId];
+                                              true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then(
+                                            function success(updatedEntityAliases) {
+                                                for (var aliasId in updatedEntityAliases) {
+                                                    var entityAlias = updatedEntityAliases[aliasId];
                                                     var datasourceIndex;
                                                     if (datasourceAliasesMap[aliasId]) {
                                                         datasourceIndex = datasourceAliasesMap[aliasId];
-                                                        datasourceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
+                                                        datasourceAliases[datasourceIndex].entityType = entityAlias.entityType;
+                                                        datasourceAliases[datasourceIndex].entityFilter = entityAlias.entityFilter;
                                                     } else if (targetDeviceAliasesMap[aliasId]) {
                                                         datasourceIndex = targetDeviceAliasesMap[aliasId];
-                                                        targetDeviceAliases[datasourceIndex].deviceFilter = deviceAlias.deviceFilter;
+                                                        targetDeviceAliases[datasourceIndex].entityType = entityAlias.entityType;
+                                                        targetDeviceAliases[datasourceIndex].entityFilter = entityAlias.entityFilter;
                                                     }
                                                 }
                                                 addImportedWidget(dashboard, widget, aliasesInfo, onAliasesUpdate, originalColumns);
@@ -477,18 +507,19 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
                     toast.showError($translate.instant('dashboard.invalid-dashboard-file-error'));
                     deferred.reject();
                 } else {
-                    var deviceAliases = dashboard.configuration.deviceAliases;
-                    if (deviceAliases) {
-                        var aliasIds = Object.keys( deviceAliases );
+                    dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
+                    var entityAliases = dashboard.configuration.entityAliases;
+                    if (entityAliases) {
+                        var aliasIds = Object.keys( entityAliases );
                         if (aliasIds.length > 0) {
-                            processDeviceAliases(deviceAliases, aliasIds).then(
-                                function(missingDeviceAliases) {
-                                    if (Object.keys( missingDeviceAliases ).length > 0) {
+                            processEntityAliases(entityAliases, aliasIds).then(
+                                function(missingEntityAliases) {
+                                    if (Object.keys( missingEntityAliases ).length > 0) {
                                         editMissingAliases($event, dashboard.configuration.widgets,
-                                                false, 'dashboard.dashboard-import-missing-aliases-title', missingDeviceAliases).then(
-                                            function success(updatedDeviceAliases) {
-                                                for (var aliasId in updatedDeviceAliases) {
-                                                    deviceAliases[aliasId] = updatedDeviceAliases[aliasId];
+                                                false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).then(
+                                            function success(updatedEntityAliases) {
+                                                for (var aliasId in updatedEntityAliases) {
+                                                    entityAliases[aliasId] = updatedEntityAliases[aliasId];
                                                 }
                                                 saveImportedDashboard(dashboard, deferred);
                                             },
@@ -534,53 +565,53 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
-    function processDeviceAliases(deviceAliases, aliasIds) {
+    function processEntityAliases(entityAliases, aliasIds) {
         var deferred = $q.defer();
-        var missingDeviceAliases = {};
+        var missingEntityAliases = {};
         var index = -1;
-        checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+        checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred);
         return deferred.promise;
     }
 
-    function checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
+    function checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred) {
         index++;
         if (index == aliasIds.length) {
-            deferred.resolve(missingDeviceAliases);
+            deferred.resolve(missingEntityAliases);
         } else {
-            checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+            checkEntityAlias(index, aliasIds, entityAliases, missingEntityAliases, deferred);
         }
     }
 
-    function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
+    function checkEntityAlias(index, aliasIds, entityAliases, missingEntityAliases, deferred) {
         var aliasId = aliasIds[index];
-        var deviceAlias = deviceAliases[aliasId];
-        deviceService.checkDeviceAlias(deviceAlias).then(
+        var entityAlias = entityAliases[aliasId];
+        entityService.checkEntityAlias(entityAlias).then(
             function(result) {
                 if (result) {
-                    checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+                    checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred);
                 } else {
-                    var missingDeviceAlias = angular.copy(deviceAlias);
-                    missingDeviceAlias.deviceFilter = null;
-                    missingDeviceAliases[aliasId] = missingDeviceAlias;
-                    checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+                    var missingEntityAlias = angular.copy(entityAlias);
+                    missingEntityAlias.entityFilter = null;
+                    missingEntityAliases[aliasId] = missingEntityAlias;
+                    checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred);
                 }
             }
         );
     }
 
-    function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
+    function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingEntityAliases) {
         var deferred = $q.defer();
         $mdDialog.show({
-            controller: 'DeviceAliasesController',
+            controller: 'EntityAliasesController',
             controllerAs: 'vm',
-            templateUrl: deviceAliasesTemplate,
+            templateUrl: entityAliasesTemplate,
             locals: {
                 config: {
-                    deviceAliases: missingDeviceAliases,
+                    entityAliases: missingEntityAliases,
                     widgets: widgets,
                     isSingleWidget: isSingleWidget,
-                    isSingleDeviceAlias: false,
-                    singleDeviceAlias: null,
+                    isSingleEntityAlias: false,
+                    singleEntityAlias: null,
                     customTitle: customTitle,
                     disableAdd: true
                 }
@@ -589,8 +620,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
             skipHide: true,
             fullscreen: true,
             targetEvent: $event
-        }).then(function (updatedDeviceAliases) {
-            deferred.resolve(updatedDeviceAliases);
+        }).then(function (updatedEntityAliases) {
+            deferred.resolve(updatedEntityAliases);
         }, function () {
             deferred.reject();
         });
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index 32395a6..be63558 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -31,12 +31,14 @@ import thingsboardDashboardAutocomplete from '../components/dashboard-autocomple
 
 import thingsboardUserMenu from './user-menu.directive';
 
+import thingsboardEntity from '../entity';
 import thingsboardTenant from '../tenant';
 import thingsboardCustomer from '../customer';
 import thingsboardUser from '../user';
 import thingsboardHomeLinks from '../home';
 import thingsboardAdmin from '../admin';
 import thingsboardProfile from '../profile';
+import thingsboardAsset from '../asset';
 import thingsboardDevice from '../device';
 import thingsboardWidgetLibrary from '../widget';
 import thingsboardDashboard from '../dashboard';
@@ -58,11 +60,13 @@ export default angular.module('thingsboard.home', [
     thingsboardMenu,
     thingsboardHomeLinks,
     thingsboardUserMenu,
+    thingsboardEntity,
     thingsboardTenant,
     thingsboardCustomer,
     thingsboardUser,
     thingsboardAdmin,
     thingsboardProfile,
+    thingsboardAsset,
     thingsboardDevice,
     thingsboardWidgetLibrary,
     thingsboardDashboard,
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 3de229e..1fc45f7 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -43,6 +43,8 @@ export default angular.module('thingsboard.locale', [])
                     "search": "Search",
                     "assign": "Assign",
                     "unassign": "Unassign",
+                    "share": "Share",
+                    "make-private": "Make private",
                     "apply": "Apply",
                     "apply-changes": "Apply changes",
                     "edit-mode": "Edit mode",
@@ -61,7 +63,8 @@ export default angular.module('thingsboard.locale', [])
                     "copy": "Copy",
                     "paste": "Paste",
                     "import": "Import",
-                    "export": "Export"
+                    "export": "Export",
+                    "share-via": "Share via {{provider}}"
                 },
                 "aggregation": {
                     "aggregation": "Aggregation",
@@ -98,6 +101,56 @@ export default angular.module('thingsboard.locale', [])
                     "enable-tls": "Enable TLS",
                     "send-test-mail": "Send test mail"
                 },
+                "asset": {
+                    "asset": "Asset",
+                    "assets": "Assets",
+                    "management": "Asset management",
+                    "view-assets": "View Assets",
+                    "add": "Add Asset",
+                    "assign-to-customer": "Assign to customer",
+                    "assign-asset-to-customer": "Assign Asset(s) To Customer",
+                    "assign-asset-to-customer-text": "Please select the assets to assign to the customer",
+                    "no-assets-text": "No assets found",
+                    "assign-to-customer-text": "Please select the customer to assign the asset(s)",
+                    "public": "Public",
+                    "assignedToCustomer": "Assigned to customer",
+                    "make-public": "Make asset public",
+                    "make-private": "Make asset private",
+                    "unassign-from-customer": "Unassign from customer",
+                    "delete": "Delete asset",
+                    "asset-public": "Asset is public",
+                    "name": "Name",
+                    "name-required": "Name is required.",
+                    "description": "Description",
+                    "type": "Type",
+                    "type-required": "Type is required.",
+                    "details": "Details",
+                    "events": "Events",
+                    "add-asset-text": "Add new asset",
+                    "asset-details": "Asset details",
+                    "assign-assets": "Assign assets",
+                    "assign-assets-text": "Assign { count, select, 1 {1 asset} other {# assets} } to customer",
+                    "delete-assets": "Delete assets",
+                    "unassign-assets": "Unassign assets",
+                    "unassign-assets-action-title": "Unassign { count, select, 1 {1 asset} other {# assets} } from customer",
+                    "assign-new-asset": "Assign new asset",
+                    "delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?",
+                    "delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.",
+                    "delete-assets-title": "Are you sure you want to delete { count, select, 1 {1 asset} other {# assets} }?",
+                    "delete-assets-action-title": "Delete { count, select, 1 {1 asset} other {# assets} }",
+                    "delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.",
+                    "make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?",
+                    "make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.",
+                    "make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?",
+                    "make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.",
+                    "unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?",
+                    "unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.",
+                    "unassign-asset": "Unassign asset",
+                    "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"
+                },
                 "attribute": {
                     "attributes": "Attributes",
                     "latest-telemetry": "Latest telemetry",
@@ -154,11 +207,19 @@ export default angular.module('thingsboard.locale', [])
                     "dashboard": "Customer Dashboard",
                     "dashboards": "Customer Dashboards",
                     "devices": "Customer Devices",
+                    "assets": "Customer Assets",
+                    "public-dashboards": "Public Dashboards",
+                    "public-devices": "Public Devices",
+                    "public-assets": "Public Assets",
                     "add": "Add Customer",
                     "delete": "Delete customer",
                     "manage-customer-users": "Manage customer users",
                     "manage-customer-devices": "Manage customer devices",
                     "manage-customer-dashboards": "Manage customer dashboards",
+                    "manage-public-devices": "Manage public devices",
+                    "manage-public-dashboards": "Manage public dashboards",
+                    "manage-customer-assets": "Manage customer assets",
+                    "manage-public-assets": "Manage public assets",
                     "add-customer-text": "Add new customer",
                     "no-customers-text": "No customers found",
                     "customer-details": "Customer details",
@@ -168,11 +229,16 @@ export default angular.module('thingsboard.locale', [])
                     "delete-customers-action-title": "Delete { count, select, 1 {1 customer} other {# customers} }",
                     "delete-customers-text": "Be careful, after the confirmation all selected customers will be removed and all related data will become unrecoverable.",
                     "manage-users": "Manage users",
+                    "manage-assets": "Manage assets",
                     "manage-devices": "Manage devices",
                     "manage-dashboards": "Manage dashboards",
                     "title": "Title",
                     "title-required": "Title is required.",
-                    "description": "Description"
+                    "description": "Description",
+                    "details": "Details",
+                    "events": "Events",
+                    "copyId": "Copy customer Id",
+                    "idCopiedMessage": "Customer Id has been copied to clipboard"
                 },
                 "datetime": {
                     "date-from": "Date from",
@@ -191,6 +257,8 @@ export default angular.module('thingsboard.locale', [])
                     "assign-to-customer-text": "Please select the customer to assign the dashboard(s)",
                     "assign-to-customer": "Assign to customer",
                     "unassign-from-customer": "Unassign from customer",
+                    "make-public": "Make dashboard public",
+                    "make-private": "Make dashboard private",
                     "no-dashboards-text": "No dashboards found",
                     "no-widgets": "No widgets configured",
                     "add-widget": "Add new widget",
@@ -219,6 +287,14 @@ export default angular.module('thingsboard.locale', [])
                     "unassign-dashboard": "Unassign dashboard",
                     "unassign-dashboards-title": "Are you sure you want to unassign { count, select, 1 {1 dashboard} other {# dashboards} }?",
                     "unassign-dashboards-text": "After the confirmation all selected dashboards will be unassigned and won't be accessible by the customer.",
+                    "public-dashboard-title": "Dashboard is now public",
+                    "public-dashboard-text": "Your dashboard <b>{{dashboardTitle}}</b> is now public and accessible via next public <a href='{{publicLink}}' target='_blank'>link</a>:",
+                    "public-dashboard-notice": "<b>Note:</b> Do not forget to make related devices public in order to access their data.",
+                    "make-private-dashboard-title": "Are you sure you want to make the dashboard '{{dashboardTitle}}' private?",
+                    "make-private-dashboard-text": "After the confirmation the dashboard will be made private and won't be accessible by others.",
+                    "make-private-dashboard": "Make dashboard private",
+                    "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.",
                     "dashboard-required": "Dashboard is required.",
@@ -248,6 +324,9 @@ export default angular.module('thingsboard.locale', [])
                     "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
                     "display-title": "Display dashboard title",
                     "title-color": "Title color",
+                    "display-entities-selection": "Display entities selection",
+                    "display-dashboard-timewindow": "Display timewindow",
+                    "display-dashboard-export": "Display export",
                     "import": "Import dashboard",
                     "export": "Export dashboard",
                     "export-failed-error": "Unable to export dashboard: {{error}}",
@@ -267,7 +346,11 @@ export default angular.module('thingsboard.locale', [])
                     "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.<br/>" +
                                               "Please contact your administrator in order to resolve this issue.",
                     "select-devices": "Select devices",
-                    "assignedToCustomer": "Assigned to customer"
+                    "assignedToCustomer": "Assigned to customer",
+                    "public": "Public",
+                    "public-link": "Public link",
+                    "copy-public-link": "Copy public link",
+                    "public-link-copied-message": "Dashboard public link has been copied to clipboard"
                 },
                 "datakey": {
                     "settings": "Settings",
@@ -279,10 +362,10 @@ export default angular.module('thingsboard.locale', [])
                     "configuration": "Data key configuration",
                     "timeseries": "Timeseries",
                     "attributes": "Attributes",
-                    "timeseries-required": "Device timeseries is required.",
-                    "timeseries-or-attributes-required": "Device timeseries/attributes is required.",
+                    "timeseries-required": "Entity timeseries are required.",
+                    "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
                     "function-types": "Function types",
-                    "function-types-required": "Function types is required."
+                    "function-types-required": "Function types are required."
                 },
                 "datasource": {
                     "type": "Datasource type",
@@ -323,6 +406,8 @@ export default angular.module('thingsboard.locale', [])
                     "assign-to-customer": "Assign to customer",
                     "assign-device-to-customer": "Assign Device(s) To Customer",
                     "assign-device-to-customer-text": "Please select the devices to assign to the customer",
+                    "make-public": "Make device public",
+                    "make-private": "Make device private",
                     "no-devices-text": "No devices found",
                     "assign-to-customer-text": "Please select the customer to assign the device(s)",
                     "device-details": "Device details",
@@ -337,6 +422,10 @@ export default angular.module('thingsboard.locale', [])
                     "unassign-devices": "Unassign devices",
                     "unassign-devices-action-title": "Unassign { count, select, 1 {1 device} other {# devices} } from customer",
                     "assign-new-device": "Assign new device",
+                    "make-public-device-title": "Are you sure you want to make the device '{{deviceName}}' public?",
+                    "make-public-device-text": "After the confirmation the device and all its data will be made public and accessible by others.",
+                    "make-private-device-title": "Are you sure you want to make the device '{{deviceName}}' private?",
+                    "make-private-device-text": "After the confirmation the device and all its data will be made private and won't be accessible by others.",
                     "view-credentials": "View credentials",
                     "delete-device-title": "Are you sure you want to delete the device '{{deviceName}}'?",
                     "delete-device-text": "Be careful, after the confirmation the device and all related data will become unrecoverable.",
@@ -369,7 +458,9 @@ export default angular.module('thingsboard.locale', [])
                     "assignedToCustomer": "Assigned to customer",
                     "unable-delete-device-alias-title": "Unable to delete device alias",
                     "unable-delete-device-alias-text": "Device alias '{{deviceAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
-                    "is-gateway": "Is gateway"
+                    "is-gateway": "Is gateway",
+                    "public": "Public",
+                    "device-public": "Device is public"
                 },
                 "dialog": {
                     "close": "Close dialog"
@@ -379,6 +470,41 @@ export default angular.module('thingsboard.locale', [])
                     "unhandled-error-code": "Unhandled error code: {{errorCode}}",
                     "unknown-error": "Unknown error"
                 },
+                "entity": {
+                    "entity": "Entity",
+                    "entities": "Entities",
+                    "aliases": "Entity aliases",
+                    "entity-alias": "Entity alias",
+                    "unable-delete-entity-alias-title": "Unable to delete entity alias",
+                    "unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
+                    "duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity aliases must be unique whithin the dashboard.",
+                    "configure-alias": "Configure '{{alias}}' alias",
+                    "alias": "Alias",
+                    "alias-required": "Entity alias is required.",
+                    "remove-alias": "Remove entity alias",
+                    "add-alias": "Add entity alias",
+                    "entity-list": "Entity list",
+                    "no-entities-matching": "No entities matching '{{entity}}' were found.",
+                    "name-starts-with": "Name starts with",
+                    "use-entity-name-filter": "Use filter",
+                    "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.",
+                    "type": "Type",
+                    "type-device": "Device",
+                    "type-asset": "Asset",
+                    "type-rule": "Rule",
+                    "type-plugin": "Plugin",
+                    "type-tenant": "Tenant",
+                    "type-customer": "Customer",
+                    "select-entities": "Select entities",
+                    "no-aliases-found": "No aliases found.",
+                    "no-alias-matching": "'{{alias}}' not found.",
+                    "create-new-alias": "Create a new one!",
+                    "no-keys-found": "No keys found.",
+                    "no-key-matching": "'{{key}}' not found.",
+                    "create-new-key": "Create a new one!"
+                },
                 "event": {
                     "event-type": "Event type",
                     "type-alarm": "Alarm",
@@ -509,7 +635,9 @@ export default angular.module('thingsboard.locale', [])
                     "export-failed-error": "Unable to export plugin: {{error}}",
                     "create-new-plugin": "Create new plugin",
                     "plugin-file": "Plugin file",
-                    "invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure."
+                    "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",
@@ -572,7 +700,9 @@ export default angular.module('thingsboard.locale', [])
                     "export-failed-error": "Unable to export rule: {{error}}",
                     "create-new-rule": "Create new rule",
                     "rule-file": "Rule file",
-                    "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure."
+                    "invalid-rule-file-error": "Unable to import rule: Invalid rule data structure.",
+                    "copyId": "Copy rule Id",
+                    "idCopiedMessage": "Rule Id has been copied to clipboard"
                 },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
@@ -594,7 +724,11 @@ export default angular.module('thingsboard.locale', [])
                     "delete-tenants-text": "Be careful, after the confirmation all selected tenants will be removed and all related data will become unrecoverable.",
                     "title": "Title",
                     "title-required": "Title is required.",
-                    "description": "Description"
+                    "description": "Description",
+                    "details": "Details",
+                    "events": "Events",
+                    "copyId": "Copy tenant Id",
+                    "idCopiedMessage": "Tenant Id has been copied to clipboard"
                 },
                 "timeinterval": {
                     "seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }",
@@ -778,8 +912,11 @@ export default angular.module('thingsboard.locale', [])
                 "language": {
                     "language": "Language",
                     "en_US": "English",
-                    "ko_KR": "Korean"
+                    "ko_KR": "Korean",
+                    "zh_CN": "Chinese",
+                    "ru_RU": "Russian",
+                    "es_ES": "Spanish"
                 }
             }
         }
-    ).name;
\ No newline at end of file
+    ).name;
diff --git a/ui/src/app/locale/locale.constant-es.js b/ui/src/app/locale/locale.constant-es.js
new file mode 100644
index 0000000..153019d
--- /dev/null
+++ b/ui/src/app/locale/locale.constant-es.js
@@ -0,0 +1,818 @@
+/*
+ * 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 function addLocaleSpanish(locales) {
+    var es_ES = {
+          "access": {
+              "unauthorized": "No autorizado",
+              "unauthorized-access": "Acceso no autorizado",
+              "unauthorized-access-text": "Debes iniciar sesión para tener acceso a este recurso!",
+              "access-forbidden": "Acceso Prohibido",
+              "access-forbidden-text": "No tienes derechos para acceder a esta ubicación!<br/>Intenta iniciar sesión con otro usuario si todavía quieres acceder a esta ubicación.",
+              "refresh-token-expired": "La sesión ha expirado",
+              "refresh-token-failed": "No se puede actualizar la sesión"
+        },
+        "action": {
+              "activate": "Activar", 
+              "suspend": "Suspender",
+              "save": "Guardar",
+              "saveAs": "Guardar como",
+              "cancel": "Cancelar",
+              "ok": "OK",
+              "delete": "Borrar",
+              "add": "Agregar",
+              "yes": "Si",
+              "no": "No",
+              "update": "Actualizar",
+              "remove": "Eliminar",
+              "search": "Buscar",
+              "assign": "Asignar",
+              "unassign": "Cancelar asignación",
+              "share": "Compartir",
+              "make-private": "Hacer privado",
+              "apply": "Aplicar",
+              "apply-changes": "Aplicar cambios",
+              "edit-mode": "Modo Edición",
+              "enter-edit-mode": "Modo Edición",
+              "decline-changes": "Descartar cambios",
+              "close": "Cerrar",
+              "back": "Atrás",
+              "run": "Correr",
+              "sign-in": "Regístrate!",
+              "edit": "Editar",
+              "view": "Ver",
+              "create": "Crear",
+              "drag": "Arrastrar",
+              "refresh": "Refrescar",
+              "undo": "Deshacer",
+              "copy": "Copiar",
+              "paste": "Pegar",
+              "import": "Importar",
+              "export": "Exportar",
+              "share-via": "Compartir vía {{provider}}"
+        },
+        "aggregation": {
+              "aggregation": "Agregación",
+              "function": "Función de Agregación",
+              "limit": "Valores Max",
+              "group-interval": "Intervalo de agrupación",
+              "min": "Min",
+              "max": "Max",
+              "avg": "Promedio",
+              "sum": "Suma",
+              "count": "Cuenta",
+              "none": "Ninguno"
+        },
+        "admin": {
+              "general": "General",
+              "general-settings": "Ajustes General",
+              "outgoing-mail": "Mail de Salida",
+              "outgoing-mail-settings": "Ajustes del Mail de Salida",
+              "system-settings": "Sistema",
+              "test-mail-sent": "Mail de prueba enviado correctamente!",
+              "base-url": "URL Base",
+              "base-url-required": "URL Base requerida.",
+              "mail-from": "Mail Desde",
+              "mail-from-required": "Mail Desde requerido.",
+              "smtp-protocol": "Protocolo SMTP",
+              "smtp-host": "Host SMTP",
+              "smtp-host-required": "Host SMTP requerido.",
+              "smtp-port": "Puerto SMTP",
+              "smtp-port-required": "Debe ingresar un Puerto SMTP.",
+              "smtp-port-invalid": "No parece un Puerto SMTP valido.",
+              "timeout-msec": "Timeout (ms)",
+              "timeout-required": "Timeout requerido.",
+              "timeout-invalid": "No parece un Timeout valido.",
+              "enable-tls": "Habilitar TLS",
+              "send-test-mail": "Enviar mail de prueba"
+        },
+        "attribute": {
+              "attributes": "Atributos",
+              "latest-telemetry": "Última telemetría",
+              "attributes-scope": "Alcance de los atributos del dispositivo",
+              "scope-latest-telemetry": "Última telemetría",
+              "scope-client": "Atributos del Cliente",
+              "scope-server": "Atributos del Servidor",
+              "scope-shared": "Atributos Compartidos",
+              "add": "Agregar atributo",
+              "key": "Clave",
+              "key-required": "Clave del atributo requerida.",
+              "value": "Valor",
+              "value-required": "Valor del atributo requerido.",
+              "delete-attributes-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 atributo} other {# atributos} }?",
+              "delete-attributes-text": "Ten cuidado, luego de confirmar el atributo será eliminado, y la información relacionada será irrecuperable.",
+              "delete-attributes": "Borrar atributo",
+              "enter-attribute-value": "Ingresar valor del atributo",
+              "show-on-widget": "Mostrar en Widget",
+              "widget-mode": "Widget",
+              "next-widget": "Widget siguiente",
+              "prev-widget": "Widget anterior",
+              "add-to-dashboard": "Agregar al Panel",
+              "add-widget-to-dashboard": "Agregar widget al Panel",
+              "selected-attributes": "{ count, select, 1 {1 atributo} other {# atributos} } seleccionados",
+              "selected-telemetry": "{ count, select, 1 {1 unidad de telemetría } other {# unidades de telemetría} } seleccionadas."
+        },
+        "confirm-on-exit": {
+              "message": "Tienes cambios sin guardar. ¿Estás seguro que quieres abandonar la página?",
+              "html-message": "Tienes cambios sin guardar.<br/>¿Estás seguro que quieres abandonar la página?",
+              "title": "Cambios sin guardar"
+        },
+        "contact": {
+              "country": "País",
+              "city": "Ciudad",
+              "state": "Estado/Provincia",
+              "postal-code": "Código Postal",
+              "postal-code-invalid": "Solo se permiten dígitos.",
+              "address": "Dirección",
+              "address2": "Dirección 2",
+              "phone": "Teléfono",
+              "email": "Email",
+              "no-address": "Sin Dirección"
+        },
+        "common": {
+              "username": "Usuario",
+              "password": "Contraseña",
+              "enter-username": "Ingresa el nombre de usuario.",
+              "enter-password": "Ingresa la contraseña",
+              "enter-search": "Ingresa búsqueda"
+        },
+        "customer": {
+              "customers": "Clientes",
+              "management": "Gestión de Clientes",
+              "dashboard": "Panel del Cliente",
+              "dashboards": "Paneles del Cliente",
+              "devices": "Panel del Cliente",
+              "public-dashboards": "Paneles Públicos",
+              "public-devices": "Dispositivos Públicos",
+              "add": "Agregar cliente",
+              "delete": "Borrar cliente",
+              "manage-customer-users": "Gestionar usuarios del cliente",
+              "manage-customer-devices": "Gestionar dispositivos del cliente",
+              "manage-customer-dashboards": "Gestionar paneles del cliente",
+              "manage-public-devices": "Gestionar dispositivos públicos",
+              "manage-public-dashboards": "Gestionar paneles públicos",
+              "add-customer-text": "Agregar nuevo cliente",
+              "no-customers-text": "No se encontrar clientes",
+              "customer-details": "Detalles del cliente",
+              "delete-customer-title": "¿Estás seguro que quieres eliminar el cliente '{{customerTitle}}'?",
+              "delete-customer-text": "Ten cuidado, luego de confirmar el cliente será eliminado y toda la información relacionada será irrecuperable.",
+              "delete-customers-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 cliente} other {# clientes} }?",
+              "delete-customers-action-title": "Borrar { count, select, 1 {1 cliente} other {# clientes} }",
+              "delete-customers-text": "Ten cuidado, luego de confirmar todos los clientes seleccionados serán eliminados y su información relacionada será irrecuperable.",
+              "manage-users": "Gestionar usuarios",
+              "manage-devices": "Gestionar dispositivos",
+              "manage-dashboards": "Gestionar paneles",
+              "title": "Título",
+              "title-required": "Título requerido.",
+              "description": "Descripción"
+        },
+        "datetime": {
+              "date-from": "Fecha desde",
+              "time-from": "Tiempo desde",
+              "date-to": "Fecha hasta",
+              "time-to": "Tiempo hasta"
+        },
+        "dashboard": {
+              "dashboard": "Panel",
+              "dashboards": "Paneles",
+              "management": "Gestión de Paneles",
+              "view-dashboards": "Ver paneles",
+              "add": "Agregar Panel",
+              "assign-dashboard-to-customer": "Asignar panel(es) a cliente",
+              "assign-dashboard-to-customer-text": "Por favor, seleccione algún panel para asignar al Cliente.",
+              "assign-to-customer-text": "Por favor, seleccione algún cliente para asignar al(los) panel(es).",
+              "assign-to-customer": "Asignar a cliente",
+              "unassign-from-customer": "Desasignar del cliente",
+              "make-public": "Hacer panel público",
+              "make-private": "Hacer panel privado",
+              "no-dashboards-text": "Ningún panel encontrado",
+              "no-widgets": "Ningún widget configurado",
+              "add-widget": "Agregar nuevo widget",
+              "title": "Titulo",
+              "select-widget-title": "Seleccionar widget",
+              "select-widget-subtitle": "Lista de tipos de widgets",
+              "delete": "Eliminar panel",
+              "title-required": "Título requerido.",
+              "description": "Descripción",
+              "details": "Detalles",
+              "dashboard-details": "Detalles del panel",
+              "add-dashboard-text": "Agregar nuevo panel",
+              "assign-dashboards": "Asignar paneles",
+              "assign-new-dashboard": "Asignar nuevo panel",
+              "assign-dashboards-text": "Asignar { count, select, 1 {1 panel} other {# paneles} } al cliente",
+              "delete-dashboards": "Eliminar paneles",
+              "unassign-dashboards": "Desasignar paneles",
+              "unassign-dashboards-action-title": "Desasignar { count, select, 1 {1 paneles} other {# paneles} } del cliente",
+              "delete-dashboard-title": "¿Estás seguro que quieres eliminar el panel '{{dashboardTitle}}'?",
+              "delete-dashboard-text": "Ten cuidado, el panel seleccionado será eliminado y la información relacionada sera irrecuperable.",
+              "delete-dashboards-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 panel} other {# paneles} }?",
+              "delete-dashboards-action-title": "Eliminar { count, select, 1 {1 panel} other {# paneles} }",
+              "delete-dashboards-text": "Ten cuidado, los paneles seleccionados serán eliminados y la información relacionada será irrecuperable.",
+              "unassign-dashboard-title": "¿Estás seguro que quieres desasignar el panel '{{dashboardTitle}}'?",
+              "unassign-dashboard-text": "Luego de confirmar, el panel será desasignado y no podrá ser accesible por el cliente.",
+              "unassign-dashboard": "Desasignar panel",
+              "unassign-dashboards-title": "¿Estás seguro que quieres desasignar { count, select, 1 {1 panel} other {# paneles} }?",
+              "unassign-dashboards-text": "Luego de confirmar, los paneles seleccionados serán desasignados y no podrán ser accesibles por el cliente.",
+              "public-dashboard-title": "El panel ahora es público",
+              "public-dashboard-text": "Tu panel <b>{{dashboardTitle}}</b> es ahora público y podrá ser accedido desde: <a href='{{publicLink}}' target='_blank'>aquí</a>:",
+              "public-dashboard-notice": "<b>Nota:</b>  No olvides hacer públicos los dispositivos relacionados para acceder a sus datos.",
+              "make-private-dashboard-title": "¿Estás seguro que quieres hacer el panel '{{dashboardTitle}}' privado?",
+              "make-private-dashboard-text": "Luego de confirmar, el panel será privado y no podrá ser accesible por otros.",
+              "make-private-dashboard": "Hacer panel privado",
+              "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
+              "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
+              "select-dashboard": "Seleccionar panel",
+              "no-dashboards-matching": "Panel '{{dashboard}}' no encontrado.",
+              "dashboard-required": "Panel requerido.",
+              "select-existing": "Seleccionar paneles existentes",
+              "create-new": "Crear nuevo panel",
+              "new-dashboard-title": "Nuevo título",
+              "open-dashboard": "Abrir panel",
+              "set-background": "Definir fondo",
+              "background-color": "Color de fondo",
+              "background-image": "Imagen de fondo",
+              "background-size-mode": "Modo tamaño de fondo",
+              "no-image": "No se ha seleccionado ningúna imagen",
+              "drop-image": "Suelte una imagen o haga clic para seleccionar un archivo para cargar.",
+              "settings": "Ajustes",
+              "columns-count": "Número de columnas",
+              "columns-count-required": "Número de columnas requerido.",
+              "min-columns-count-message": "Solo se permite un número mínimo de 10 columnas.",
+              "max-columns-count-message": "Solo se permite un número máximo de 1000 columnas.",
+              "widgets-margins": "Margen entre widgets",
+              "horizontal-margin": "Margen horizontal",
+              "horizontal-margin-required": "Margen horizontal requerido.",
+              "min-horizontal-margin-message": "Solo se permite margen horizontal mínimo de 0.",
+              "max-horizontal-margin-message": "Solo se permite margen horizontal máximo de 50.",
+              "vertical-margin": "Margen vertical",
+              "vertical-margin-required": "Margen vertical requerido.",
+              "min-vertical-margin-message": "Solo se permite margen vertical mínimo de 0.",
+              "max-vertical-margin-message": "Solo se permite margen vertical máximo de 50.",
+              "display-title": "Mostrar título del panel",
+              "title-color": "Color del título",
+              "display-device-selection": "Mostrar selección de dispositivo",
+              "display-dashboard-timewindow": "Mostrar ventana de tiempo",
+              "display-dashboard-export": "Mostrar exportar",
+              "import": "Importar panel",
+              "export": "Exportar panel",
+              "export-failed-error": "Imposible exportar panel: {{error}}",
+              "create-new-dashboard": "Crear nuevo panel",
+              "dashboard-file": "Archivo del panel",
+              "invalid-dashboard-file-error": "Imposible importar panel: Estructura de datos inválida.",
+              "dashboard-import-missing-aliases-title": "Configurar alias utilizados por el panel importado",
+              "create-new-widget": "Crear nuevo widget",
+              "import-widget": "Importar widget",
+              "widget-file": "Archivo de widget",
+              "invalid-widget-file-error": "Imposible importar widget: Estructura de datos inválida.",
+              "widget-import-missing-aliases-title": "Configurar alias utilizados por el widget",
+              "open-toolbar": "Abrir toolbar del panel",
+              "close-toolbar": "Cerrar toolbar",
+              "configuration-error": "Error de configuración",
+              "alias-resolution-error-title": "Error de configuración de alias del panel",
+              "invalid-aliases-config": "No se puede encontrar ningún dispositivo que coincida con algunos de los alias de filtro.<br/>" +
+              "Póngase en contacto con su administrador para resolver este problema.",
+              "select-devices": "Seleccionar dispositivos",
+              "assignedToCustomer": "Asignado al cliente",
+              "public": "Público",
+              "public-link": "Link público",
+              "copy-public-link": "Copiar link público",
+              "public-link-copied-message": "El link público del panel se ha copiado al portapapeles"
+        },
+        "datakey": {
+              "settings": "Ajustes",
+              "advanced": "Avanzado",
+              "label": "Etiqueta",
+              "color": "Color",
+              "data-generation-func": "Función de generación de datos",
+              "use-data-post-processing-func": "Usar funcíon de post-procesamiendo de datos",
+              "configuration": "Ajustes de clave de datos",
+              "timeseries": "Serie de tiempos",
+              "attributes": "Atributos",
+              "timeseries-required": "Series de tiempo del dispositivo requerido.",
+              "timeseries-or-attributes-required": "Series de tiempo/Atributos requeridos.",
+              "function-types": "Tipos de funciones",
+              "function-types-required": "Tipos de funciones requerido."
+        },
+        "datasource": {
+              "type": "Típo de fuente de datos",
+              "add-datasource-prompt": "Por favor, agrega una fuente de datos"
+        },
+        "details": {
+              "edit-mode": "Modo Edición",
+              "toggle-edit-mode": "Ir a Modo Edición"
+        },
+        "device": {
+              "device": "Dispositivo",
+              "device-required": "Dispositivo requerido.",
+              "devices": "Dispositivos",
+              "management": "Gestión de Dispositivos",
+              "view-devices": "Ver dispositivos",
+              "device-alias": "Alias de dispositivo",
+              "aliases": "Alias de dispositivos",
+              "no-alias-matching": "'{{alias}}' no encontrado.",
+              "no-aliases-found": "Ningún alias encontrado.",
+              "no-key-matching": "'{{key}}' no encontrado.",
+              "no-keys-found": "Ninguna clave encontrada.",
+              "create-new-alias": "Crear nuevo alias!",
+              "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}}'",
+              "alias": "Alias",
+              "alias-required": "Alias de dispositivo requerido.",
+              "remove-alias": "Eliminar alias",
+              "add-alias": "Agregar alias",
+              "name-starts-with": "Nombre empieza con",
+              "device-list": "Lista de dispositivos",
+              "use-device-name-filter": "Usar filtro",
+              "device-list-empty": "Ningún dispositivo seleccionado.",
+              "device-name-filter-required": "Nombre de filtro requerido.",
+              "device-name-filter-no-device-matched": "Ningún dispositivo encontrado que comience con '{{device}}'.",
+              "add": "Agregar dispositivo",
+              "assign-to-customer": "Asignar a cliente",
+              "assign-device-to-customer": "Asignar dispositivo(s) a Cliente",
+              "assign-device-to-customer-text": "Por favor, seleccione los dispositivos que serán asignados al cliente",
+              "make-public": "Hacer dispositivo público",
+              "make-private": "Hacer dispositivo privado",
+              "no-devices-text": "Ningún dispositivo encontrado",
+              "assign-to-customer-text": "Por favor, seleccione el cliente para asignar el(los) dispositivo(s)",
+              "device-details": "Detalles del dispositivo",
+              "add-device-text": "Agregar nuevo dispositivo",
+              "credentials": "Credenciales",
+              "manage-credentials": "Gestionar credenciales",
+              "delete": "Eliminar dispositivo",
+              "assign-devices": "Asignar dispositivo",
+              "assign-devices-text": "Asignar { count, select, 1 {1 dispositivo} other {# dispositivos} } al cliente",
+              "delete-devices": "Eliminar dispositivo",
+              "unassign-from-customer": "Desasignar del cliente",
+              "unassign-devices": "Desasignar dispositivos",
+              "unassign-devices-action-title": "Desasignar { count, select, 1 {1 dispositivo} other {# dispositivos} } del cliente",
+              "assign-new-device": "Asignar nuevo dispositivo",
+              "make-public-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' público?",
+              "make-public-device-text": "Luego de confirmar, el dispositivo y la información relacionada serán públicos y podrá ser accesible por otros.",
+              "make-private-device-title": "¿Estás seguro que quieres hacer el dispositivo '{{deviceName}}' privado?",
+              "make-private-device-text": "Luego de confirmar, el dispositivo y la información relacionada serán privados y no podrá ser accesible por otros.",
+              "view-credentials": "Ver credenciales",
+              "delete-device-title": "¿Estás seguro que quieres eliminar el dispositivo '{{deviceName}}'?",
+              "delete-device-text": "Ten cuidado, luego de confirmar los dispositivos serán eliminados y la información relacionada será irrecuperable.",
+              "delete-devices-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 dispositivo} other {# dispositivos} }?",
+              "delete-devices-action-title": "Eliminar { count, select, 1 {1 dispositivo} other {# dispositivos} }",
+              "delete-devices-text": "Ten cuidado, luego de confirmar los dispositivos seleccionados serán eliminados y la información relacionada será irrecuperable.",
+              "unassign-device-title": "¿Estás seguro que quieres desasignar el dispositivo '{{deviceName}}'?",
+              "unassign-device-text": "Luego de confirmar el dispositivo será desasignado y no podrá ser accesible por el cliente.",
+              "unassign-device": "Desasignar dispositivo",
+              "unassign-devices-title": "¿Estás seguro que quieres desasignar { count, select, 1 {1 dispositivo} other {# dispositivos} }?",
+              "unassign-devices-text": "Luego de confirmar los dispositivos seleccionados serán desasignados y no podrán ser accedidos por el cliente.",
+              "device-credentials": "Credenciales del dispositivo",
+              "credentials-type": "Tipo de credencial",
+              "access-token": "Access token",
+              "access-token-required": "Access token requerido.",
+              "access-token-invalid": "Access token debe tener entre 1 a 20 caracteres.",
+              "rsa-key": "Clave pública RSA",
+              "rsa-key-required": "Clave pública RSA requerida.",
+              "secret": "Secreta",
+              "secret-required": "Secreta requerida.",
+              "name": "Nombre",
+              "name-required": "Nombre requerido.",
+              "description": "Descripción",
+              "events": "Eventos",
+              "details": "Detalles",
+              "copyId": "Copiar ID",
+              "copyAccessToken": "Copiar access token",
+              "idCopiedMessage": "Id del dispositivo copiado al portapapeles",
+              "accessTokenCopiedMessage": "Access token del dispositivo copiado al portapapeles",
+              "assignedToCustomer": "Asignado al cliente",
+              "unable-delete-device-alias-title": "Imposible eliminar alias del dispositivo",
+              "unable-delete-device-alias-text": "Alias '{{deviceAlias}}' no puede ser eliminado. Esta siendo usado por el(los) widget(s):<br/>{{widgetsList}}",
+              "is-gateway": "Es gateway",
+              "public": "Público",
+              "device-public": "Dispositivo público"
+        },
+        "dialog": {
+              "close": "Cerrar cuadro de diálogo"
+        },
+        "error": {
+              "unable-to-connect": "Imposible conectar con el servidor! Por favor, revise su conexión a internet.",
+              "unhandled-error-code": "Código de error no manejado: {{errorCode}}",
+              "unknown-error": "Error desconocido"
+        },
+        "event": {
+              "event-type": "Tipo de evento",
+              "type-alarm": "Alarma",
+              "type-error": "Error",
+              "type-lc-event": "Ciclo de vida",
+              "type-stats": "Estadísticas",
+              "no-events-prompt": "Ningún evento encontrado.",
+              "error": "Error",
+              "alarm": "Alarma",
+              "event-time": "Hora del evento",
+              "server": "Servidor",
+              "body": "Cuerpo",
+              "method": "Método",
+              "event": "Evento",
+              "status": "Status",
+              "success": "Éxito",
+              "failed": "Fallo",
+              "messages-processed": "Mensajes procesados",
+              "errors-occurred": "Ocurrieron errores"
+        },
+        "fullscreen": {
+              "expand": "Expandir a Pantalla Completa",
+              "exit": "Salir de Pantalla Completa",
+              "toggle": "Cambiar el modo de Pantalla Completa",
+              "fullscreen": "Pantalla Completa"
+        },
+        "function": {
+              "function": "Función"
+        },
+        "grid": {
+              "delete-item-title": "¿Estás seguro que quieres eliminar este item?",
+              "delete-item-text": "Ten cuidado, luego de confirmar el item será eliminado y la información relacionada será irrecuperable.",
+              "delete-items-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 item} other {# items} }?",
+              "delete-items-action-title": "Eliminar { count, select, 1 {1 item} other {# items} }",
+              "delete-items-text": "Ten cuidado, luego de confirmar los items seleccionados serán eliminados y la información relacionada será irrecuperable.",
+              "add-item-text": "Agregar nuevo item",
+              "no-items-text": "Ningún item encontrado",
+              "item-details": "Detalles del item",
+              "delete-item": "Borrar Item",
+              "delete-items": "Borrar Items",
+              "scroll-to-top": "Ir hacia arriba"
+        },
+        "help": {
+              "goto-help-page": "Ir a Página de Ayuda"
+        },
+        "home": {
+              "home": "Principal",
+              "profile": "Perfil",
+              "logout": "Salir",
+              "menu": "Menu",
+              "avatar": "Avatar",
+              "open-user-menu": "Abrir menú de usuario"
+        },
+        "import": {
+              "no-file": "Ningún archivo seleccionado",
+              "drop-file": "Arrastra un archivo JSON o clickea para seleccionar uno."
+        },
+        "item": {
+              "selected": "Seleccionado"
+        },
+        "js-func": {
+              "no-return-error": "La función debe retornar un valor!",
+              "return-type-mismatch": "La función debe retornar un valor de tipo: '{{type}}'!"
+        },
+        "legend": {
+              "position": "Posición de leyenda",
+              "show-max": "Mostrar máximo",
+              "show-min": "Mostrar mínimo",
+              "show-avg": "Mostrar promedio",
+              "show-total": "Mostrar total",
+              "settings": "Ajustes de leyenda.",
+              "min": "min",
+              "max": "max",
+              "avg": "prom",
+              "total": "total"
+        },
+        "login": {
+              "login": "Ingresar",
+              "request-password-reset": "Pedir restablecer contraseña",
+              "reset-password": "Restablecer contraseña",
+              "create-password": "Crear contraseña",
+              "passwords-mismatch-error": "Las contraseñas deben ser las mismas!",
+              "password-again": "Reingresa la contraseña",
+              "sign-in": "Iniciar sesión",
+              "username": "Usuario (email)",
+              "remember-me": "Recordar",
+              "forgot-password": "¿Olvidaste tu contraseña?",
+              "password-reset": "Restablecer Contraseña",
+              "new-password": "Nueva contraseña",
+              "new-password-again": "Repita la nueva contraseña",
+              "password-link-sent-message": "Se ha enviado el enlace de restablecimiento de contraseña con éxito!",
+              "email": "Email"
+        },
+        "plugin": {
+              "plugins": "Plugins",
+              "delete": "Eliminar plugin",
+              "activate": "Activar plugin",
+              "suspend": "Suspender plugin",
+              "active": "Activo",
+              "suspended": "Suspendido",
+              "name": "Nombre",
+              "name-required": "Nombre requerido.",
+              "description": "Descripción",
+              "add": "Agregar Plugin",
+              "delete-plugin-title": "¿Estás seguro que quieres eliminar el plugin '{{pluginName}}'?",
+              "delete-plugin-text": "Ten cuidado, luego de confirmar el plugin será eliminado y la información relacionada será irrecuperable.",
+              "delete-plugins-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 plugin} other {# plugins} }?",
+              "delete-plugins-action-title": "Eliminar { count, select, 1 {1 plugin} other {# plugins} }",
+              "delete-plugins-text": "Ten cuidado, luego de confirmar todos los plugins seleccionados serán eliminados y la información relacionada será irrecuperable.",
+              "add-plugin-text": "Agregar nuevo plugin",
+              "no-plugins-text": "Ningún plugin encontrado",
+              "plugin-details": "Detalles",
+              "api-token": "API token",
+              "api-token-required": "API token requerido.",
+              "type": "Tipo del plugin",
+              "type-required": "Tipo requerido.",
+              "configuration": "Ajustes del plugin",
+              "system": "Sistema",
+              "select-plugin": "plugin",
+              "plugin": "Plugin",
+              "no-plugins-matching": "No se encontraron plugins: '{{plugin}}'",
+              "plugin-required": "Plugin requerido.",
+              "plugin-require-match": "Por favor, elija un plugin existente.",
+              "events": "Eventos",
+              "details": "Detalles",
+              "import": "Importar plugin",
+              "export": "Exportar plugin",
+              "export-failed-error": "Imposible exportar plugin: {{error}}",
+              "create-new-plugin": "Crear nuevo plugin",
+              "plugin-file": "Archivo",
+              "invalid-plugin-file-error": "Imposible de importar plugin: Estructura de datos inválida."
+        },
+        "position": {
+              "top": "Arriba",
+              "bottom": "Abajo",
+              "left": "Izquierda",
+              "right": "Derecha"
+        },
+        "profile": {
+              "profile": "Perfil",
+              "change-password": "Cambiar contraseña",
+              "current-password": "Contraseña actual"
+        },
+        "rule": {
+              "rules": "Reglas",
+              "delete": "Eliminar regla",
+              "activate": "Activar regla",
+              "suspend": "Suspender regla",
+              "active": "Activada",
+              "suspended": "Suspendida",
+              "name": "Nombre",
+              "name-required": "Nombre requerido.",
+              "description": "Descripción",
+              "add": "Agregar Regla",
+              "delete-rule-title": "¿Estás seguro que quieres eliminar la regla '{{ruleName}}'?",
+              "delete-rule-text": "Ten cuidado, luego de confirmar la regla será eliminada y la información relacionada será irrecuperable.",
+              "delete-rules-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 regla} other {# reglas} }?",
+              "delete-rules-action-title": "Eliminar { count, select, 1 {1 regla} other {# reglas} }",
+              "delete-rules-text": "Ten cuidado, luego de confirmar todas las reglas seleccionadas serán borradas y la información relacionada será irrecuperable.",
+              "add-rule-text": "Agregar nueva regla",
+              "no-rules-text": "Ninguna regla encontrada",
+              "rule-details": "Detalles",
+              "filters": "Filtros",
+              "filter": "Filtro",
+              "add-filter-prompt": "Por favor, ingresa un filtro",
+              "remove-filter": "Eliminar filtro",
+              "add-filter": "Agregar filtro",
+              "filter-name": "Nombre",
+              "filter-type": "Tipo",
+              "edit-filter": "Editar filtro",
+              "view-filter": "Ver filtro",
+              "component-name": "Nombre",
+              "component-name-required": "Nombre requerido.",
+              "component-type": "Tipo",
+              "component-type-required": "Tipo requerido.",
+              "processor": "Procesador",
+              "no-processor-configured": "Ningún procesador encontrado",
+              "create-processor": "Crear procesador",
+              "processor-name": "Nombre",
+              "processor-type": "Tipo",
+              "plugin-action": "Acción del Plugin",
+              "action-name": "Nombre",
+              "action-type": "Tipo",
+              "create-action-prompt": "Por favor, crea una acción.",
+              "create-action": "Crear acción",
+              "details": "Detalles",
+              "events": "Eventos",
+              "system": "Sistema",
+              "import": "Importar regla",
+              "export": "Exportar regla",
+              "export-failed-error": "Imposible de exportar regla: {{error}}",
+              "create-new-rule": "Crear nueva regla",
+              "rule-file": "Archivo",
+              "invalid-rule-file-error": "Imposible de importar regla: Estructura de datos inválida."
+        },
+        "rule-plugin": {
+              "management": "Gestión de Reglas y Plugins"
+        },
+        "tenant": {
+              "tenants": "Tenants",
+              "management": "Gestión de Tenant",
+              "add": "Agregar Tenant",
+              "admins": "Admins",
+              "manage-tenant-admins": "Gestionar administradores tenant",
+              "delete": "Eliminar tenant",
+              "add-tenant-text": "Agregar nuevo tenant",
+              "no-tenants-text": "Ningún tenant encontrado",
+              "tenant-details": "Detalles del Tenant",
+              "delete-tenant-title": "¿Estás seguro que quieres eliminar el tenant '{{tenantTitle}}'?",
+              "delete-tenant-text": "Ten cuidado, luego de confirmar el tenant será eliminado y la información relacionada será irrecuperable.",
+              "delete-tenants-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 tenant} other {# tenants} }?",
+              "delete-tenants-action-title": "Eliminar { count, select, 1 {1 tenant} other {# tenants} }",
+              "delete-tenants-text": "Ten cuidado, luego de confirmar los tenants seleccionados serán eliminados y la información relacionada será irrecuperable.",
+              "title": "Título",
+              "title-required": "Título requerido.",
+              "description": "Descripción"
+        },
+        "timeinterval": {
+              "seconds-interval": "{ seconds, select, 1 {1 segundo} other {# segundos} }",
+              "minutes-interval": "{ minutes, select, 1 {1 minuto} other {# minutos} }",
+              "hours-interval": "{ hours, select, 1 {1 hora} other {# horas} }",
+              "days-interval": "{ days, select, 1 {1 día} other {# días} }",
+              "days": "Días",
+              "hours": "Horas",
+              "minutes": "Minutos",
+              "seconds": "Segundos",
+              "advanced": "Avanzado"
+        },
+        "timewindow": {
+              "days": "{ days, select, 1 { día } other {# días } }",
+              "hours": "{ hours, select, 0 { horas } 1 {1 hora } other {# horas } }",
+              "minutes": "{ minutes, select, 0 { minutos } 1 {1 minuto } other {# minutos } }",
+              "seconds": "{ seconds, select, 0 { segundos } 1 {1 segundo } other {# segundos } }",
+              "realtime": "Tiempo-real",
+              "history": "Histórico",
+              "last-prefix": "último",
+              "period": "desde {{ startTime }} hasta {{ endTime }}",
+              "edit": "Editar ventana de tiempo",
+              "date-range": "Rango de fechas",
+              "last": "Últimos",
+              "time-period": "Período de tiempo"
+        },
+        "user": {
+              "users": "Usuarios",
+              "customer-users": "Usuarios del Cliente",
+              "tenant-admins": "Tenant Admins",
+              "sys-admin": "Administrador del Sistema",
+              "tenant-admin": "Administrador Tenant",
+              "customer": "Cliente",
+              "anonymous": "Anónimo",
+              "add": "Agregar usuario",
+              "delete": "Eliminar usuario",
+              "add-user-text": "Agregar nuevo usuario",
+              "no-users-text": "Ningún usuario encontrado",
+              "user-details": "Detalles del usuario",
+              "delete-user-title": "¿Estás seguro que quieres eliminar el usuario '{{userEmail}}'?",
+              "delete-user-text": "Ten cuidado, luego de confirmar el usuario seleccionado será eliminado y la información relacionada será irrecuperable.",
+              "delete-users-title": "¿Estás seguro que quieres eliminar { count, select, 1 {1 usuario} other {# usuarios} }?",
+              "delete-users-action-title": "Borrar { count, select, 1 {1 usuario} other {# usuarios} }",
+              "delete-users-text": "Ten cuidado, luego de confirmar los usuarios seleccionados serán eliminados y la información relacionada será irrecuperable.",
+              "activation-email-sent-message": "Mail de activación enviado con éxito!",
+              "resend-activation": "Reenviar activación",
+              "email": "Email",
+              "email-required": "Email requerido.",
+              "first-name": "Nombre",
+              "last-name": "Apellido",
+              "description": "Descripción",
+              "default-dashboard": "Panel por defecto",
+              "always-fullscreen": "Siempre en pantalla completa"
+        },
+        "value": {
+              "type": "Tipo de valor",
+              "string": "Cadena de texto",
+              "string-value": "Valor de cadena de texto",
+              "integer": "Nro entero",
+              "integer-value": "Valor de nro entero",
+              "invalid-integer-value": "Valor inválido",
+              "double": "Nro decimal",
+              "double-value": "Valor nro decimal",
+              "boolean": "Booleano",
+              "boolean-value": "Valor booleano",
+              "false": "Falso",
+              "true": "Verdadero"
+        },
+        "widget": {
+              "widget-library": "Bibloteca de Widgets",
+              "widget-bundle": "Paquetes de Widgets",
+              "select-widgets-bundle": "Seleccionar paquete de widgets",
+              "management": "Gestión de Widgets",
+              "editor": "Editor de widgets",
+              "widget-type-not-found": "Problema al cargar la configuración del widget.<br>Probablemente asociado\n    El tipo de widget fue eliminado.",
+              "widget-type-load-error": "Widget no pudo ser cargado debido a estos errores:",
+              "remove": "Eliminar widget",
+              "edit": "Editar widget",
+              "remove-widget-title": "¿Estás seguro que quieres eliminar el widget '{{widgetTitle}}'?",
+              "remove-widget-text": "Luego de confirmar el widget será eliminado y toda la información relacionada será irrecuperable..",
+              "timeseries": "Series de tiempo",
+              "latest-values": "Últimos valores",
+              "rpc": "Widget de control",
+              "static": "Widget estático",
+              "select-widget-type": "Seleccionar tipo de widget",
+              "missing-widget-title-error": "El titulo del widget debe ser especificado!",
+              "widget-saved": "Widget guardado",
+              "unable-to-save-widget-error": "Imposible guardar widget! Tiene errores!",
+              "save": "Guardar widget",
+              "saveAs": "Guardar widget como",
+              "save-widget-type-as": "Guardar tipo de widget como",
+              "save-widget-type-as-text": "Por favor, ingrese un nuevo titulo y/o seleccione un paquete de destino.",
+              "toggle-fullscreen": "Cambiar a pantalla completa",
+              "run": "Correr widget",
+              "title": "Titulo",
+              "title-required": "Titulo requerido.",
+              "type": "Tipo",
+              "resources": "Recursos",
+              "resource-url": "JavaScript/CSS URL",
+              "remove-resource": "Eliminar recurso",
+              "add-resource": "Agregar recurso",
+              "html": "HTML",
+              "tidy": "Tidy",
+              "css": "CSS",
+              "settings-schema": "Esquema de configuración",
+              "datakey-settings-schema": "Esquema de configuración de clave de datos",
+              "javascript": "Javascript",
+              "remove-widget-type-title": "¿Estás seguro que quieres eliminar el tipo del widget '{{widgetName}}'?",
+              "remove-widget-type-text": "Luego de confirmar el tipo será eliminado y la información relacionada será irrecuperable.",
+              "remove-widget-type": "Eliminar tipo de widget.",
+              "add-widget-type": "Agregar nuevo tipo de widget",
+              "widget-type-load-failed-error": "Error al cargar el tipo de widget!",
+              "widget-template-load-failed-error": "Error al cargar el template del widget!",
+              "add": "Agregar Widget",
+              "undo": "Deshacer cambios",
+              "export": "Exportar widget"
+        },
+        "widgets-bundle": {
+              "current": "Paquete actual",
+              "widgets-bundles": "Paquete de Widgets",
+              "add": "Agregar paquete de widgets",
+              "delete": "Eliminar paquete de widgets",
+              "title": "Título",
+              "title-required": "Título requerido.",
+              "add-widgets-bundle-text": "Agregar nuevo paquete de widgets",
+              "no-widgets-bundles-text": "Ningún paquete de widgets encontrado",
+              "empty": "Paquete de widgets vacío.",
+              "details": "Detalles",
+              "widgets-bundle-details": "Detalles del paquete de Widgets",
+              "delete-widgets-bundle-title": "¿Estás seguro que  desea eliminar el paquete de widgets '{{widgetsBundleTitle}}'?",
+              "delete-widgets-bundle-text": "Ten cuidado, luego de confirmar todos los paquetes seleccionados serán eliminados y su información relacionada será irrecuperable.",
+              "delete-widgets-bundles-title": "¿Estás seguro que deseas eliminar { count, select, 1 {1 paquete de widgets} other {# paquetes de widgets} }?",
+              "delete-widgets-bundles-action-title": "Eliminar { count, select, 1 {1 paquete de widgets} other {# paquetes de widgets} }",
+              "delete-widgets-bundles-text": "Ten cuidado, luego de confirmar todos los paquetes seleccionados serán eliminados y la información relacionada será irrecuperable.",
+              "no-widgets-bundles-matching": "Ningún paquete '{{widgetsBundle}}' encontrado.",
+              "widgets-bundle-required": "Paquete de widget requerido.",
+              "system": "Sistema",
+              "import": "Importar paquete de widgets",
+              "export": "Exportar paquete de widgets",
+              "export-failed-error": "Imposible exportar paquete de widgets: {{error}}",
+              "create-new-widgets-bundle": "Crear nuevo paquete de widgets",
+              "widgets-bundle-file": "Archivo de paquete de widgets",
+              "invalid-widgets-bundle-file-error": "Imposible importar paquete de widgets: Estructura de datos inválida."
+        },
+        "widget-config": {
+              "data": "Datos",
+              "settings": "Ajustes",
+              "advanced": "Avanzado",
+              "title": "Titulo",
+              "general-settings": "Ajustes generales",
+              "display-title": "Mostrar titulo",
+              "drop-shadow": "Sombra",
+              "enable-fullscreen": "Habilitar pantalla completa",
+              "background-color": "Color de fondo",
+              "text-color": "Color del texto",
+              "padding": "Relleno",
+              "title-style": "Estilo de título",
+              "mobile-mode-settings": "Ajustes mobile.",
+              "order": "Orden",
+              "height": "Altura",
+              "units": "Caracter especial a mostrar en el siguiente valor",
+              "decimals": "Números de dígitos después de la coma",
+              "timewindow": "Ventana de tiempo",
+              "use-dashboard-timewindow": "Usar ventana de tiempo del Panel",
+              "display-legend": "Mostrar leyenda",
+              "datasources": "Set de datos",
+              "datasource-type": "Tipo",
+              "datasource-parameters": "Parámetros",
+              "remove-datasource": "Eliminar set de datos",
+              "add-datasource": "Agregar set de datos",
+              "target-device": "Dispositivo destino"
+        },
+        "widget-type": {
+              "import": "Importar tipo de widget",
+              "export": "Exportar tipo de widget",
+              "export-failed-error": "Imposible exportar tipo de widget: {{error}}",
+              "create-new-widget-type": "Crear nuevo tipo de widget",
+              "widget-type-file": "Tipo de archivo del widget",
+              "invalid-widget-type-file-error": "Imposible de importar tipo de widget: Estructura de datos inválida."
+        },
+        "language": {
+              "language": "Lenguaje",
+              "en_US": "Inglés",
+              "ko_KR": "Coreano",
+              "zh_CN": "Chino",
+              "ru_RU": "Ruso",
+              "es_ES": "Español"
+        }
+  };
+  angular.extend(locales, {'es_ES': es_ES});
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-ko.js b/ui/src/app/locale/locale.constant-ko.js
index bdc9388..8b2d4ba 100644
--- a/ui/src/app/locale/locale.constant-ko.js
+++ b/ui/src/app/locale/locale.constant-ko.js
@@ -775,7 +775,10 @@ export default function addLocaleKorean(locales) {
         "language": {
             "language": "언어",
             "en_US": "영어",
-            "ko_KR": "한글"
+            "ko_KR": "한글",
+            "zh_CN": "중국어",
+            "ru_RU": "러시아어",
+            "es_ES": "스페인어"
         }
     };
     angular.extend(locales, {'ko_KR': ko_KR});
diff --git a/ui/src/app/locale/locale.constant-ru.js b/ui/src/app/locale/locale.constant-ru.js
new file mode 100644
index 0000000..47c9460
--- /dev/null
+++ b/ui/src/app/locale/locale.constant-ru.js
@@ -0,0 +1,819 @@
+/*
+ * 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 function addLocaleRussian(locales) {
+    var ru_RU = {
+        "access": {
+            "unauthorized": "Неавторизированный",
+            "unauthorized-access": "Несанкционированный доступ",
+            "unauthorized-access-text": "Вы должны войти в систему для получения доступа к этому ресурсу!",
+            "access-forbidden": "Доступ запрещен",
+            "access-forbidden-text": "У вас нет прав доступа к этому ресурсу!<br/>Для получения доступа попробуйте войти под другим пользователем.",
+            "refresh-token-expired": "Сессия истекла",
+            "refresh-token-failed": "Не удалось обновить сессию"
+        },
+        "action": {
+            "activate": "Активировать",
+            "suspend": "Приостановить",
+            "save": "Сохранить",
+            "saveAs": "Сохранить как",
+            "cancel": "Отмена",
+            "ok": "ОК",
+            "delete": "Удалить",
+            "add": "Добавить",
+            "yes": "Да",
+            "no": "Нет",
+            "update": "Обновить",
+            "remove": "Удалить",
+            "search": "Поиск",
+            "assign": "Присвоить",
+            "unassign": "Отменить присвоение",
+            "share": "Поделиться",
+            "make-private": "Закрыть для общего доступа",
+            "apply": "Применить",
+            "apply-changes": "Применить изменения",
+            "edit-mode": "Режим редактирования",
+            "enter-edit-mode": "Режим редактирования",
+            "decline-changes": "Отменить изменения",
+            "close": "Закрыть",
+            "back": "Назад",
+            "run": "Запуск",
+            "sign-in": "Войти",
+            "edit": "Редактировать",
+            "view": "Просмотреть",
+            "create": "Создать",
+            "drag": "Переместить",
+            "refresh": "Обновить",
+            "undo": "Откатить",
+            "copy": "Копировать",
+            "paste": "Вставить",
+            "import": "Импортировать",
+            "export": "Экспортировать",
+            "share-via": "Поделиться в {{provider}}"
+        },
+        "aggregation": {
+            "aggregation": "Агрегация",
+            "function": "Тип агрегации данных",
+            "limit": "Максимальное значение",
+            "group-interval": "Интервал группировки",
+            "min": "Мин",
+            "max": "Maкс",
+            "avg": "Среднее",
+            "sum": "Сумма",
+            "count": "Количество",
+            "none": "Без агрегации"
+        },
+        "admin": {
+            "general": "Общие",
+            "general-settings": "Общие настройки",
+            "outgoing-mail": "Исходящая почта",
+            "outgoing-mail-settings": "Настройки исходящей почты",
+            "system-settings": "Системные настройки",
+            "test-mail-sent": "Пробное письмо успешно отправлено!",
+            "base-url": "Базовая URL",
+            "base-url-required": "Базовая URL обязательна.",
+            "mail-from": "Отправитель",
+            "mail-from-required": "Отправитель обязателен.",
+            "smtp-protocol": "SMTP протокол",
+            "smtp-host": "SMTP хост",
+            "smtp-host-required": "SMTP хост обязателен.",
+            "smtp-port": "SMTP порт",
+            "smtp-port-required": "SMTP порт обязателен.",
+            "smtp-port-invalid": "Недействительный SMTP порт.",
+            "timeout-msec": "Таймаут (мс)",
+            "timeout-required": "Таймаут обязателен.",
+            "timeout-invalid": "Недействительный таймаут.",
+            "enable-tls": "Включить TLS",
+            "send-test-mail": "Отправить пробное письмо"
+        },
+        "attribute": {
+            "attributes": "Атрибуты",
+            "latest-telemetry": "Последняя телеметрия",
+            "attributes-scope": "Контекст атрибутов устройства",
+            "scope-latest-telemetry": "Последняя телеметрия",
+            "scope-client": "Клиентские атрибуты",
+            "scope-server": "Серверные атрибуты",
+            "scope-shared": "Общие атрибуты",
+            "add": "Добавить атрибут",
+            "key": "Ключ",
+            "key-required": "Ключ атрибута обязателен.",
+            "value": "Значение",
+            "value-required": "Значение атрибута обязательно.",
+            "delete-attributes-title": "Вы уверенны, что хотите удалить { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }? ",
+            "delete-attributes-text": "Внимание, после подтверждения выбранные атрибуты будут удалены.",
+            "delete-attributes": "Удалить атрибуты",
+            "enter-attribute-value": "Введите значение атрибута",
+            "show-on-widget": "Показать на виджете",
+            "widget-mode": "Виджет-режим",
+            "next-widget": "Следующий виджет",
+            "prev-widget": "Предыдущий виджет",
+            "add-to-dashboard": "Добавить на дашборд",
+            "add-widget-to-dashboard": "Добавить виджет на дашборд",
+            "selected-attributes": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }",
+            "selected-telemetry": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, 1 {1 параметр} few {# параметра} other {# параметров} } телеметрии"
+        },
+        "confirm-on-exit": {
+            "message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?",
+            "html-message": "У вас есть несохраненные изменения.<br/>Вы точно хотите покинуть эту страницу?",
+            "title": "Несохраненные изменения"
+        },
+        "contact": {
+            "country": "Страна",
+            "city": "Город",
+            "state": "Штат",
+            "postal-code": "Почтовый код",
+            "postal-code-invalid": "Допустимы только цифры",
+            "address": "Адрес",
+            "address2": "Адрес 2",
+            "phone": "Телефон",
+            "email": "Эл. адрес",
+            "no-address": "Адрес не указан"
+        },
+        "common": {
+            "username": "Имя пользователя",
+            "password": "Пароль",
+            "enter-username": "Введите имя пользователя",
+            "enter-password": "Введите пароль",
+            "enter-search": "Введите условие поиска"
+        },
+        "customer": {
+            "customers": "Клиенты",
+            "management": "Управление клиентами",
+            "dashboard": "Дашборд клиентов",
+            "dashboards": "Дашборды клиентов",
+            "devices": "Устройства клиента",
+            "public-dashboards": "Общедоступные дашборды",
+            "public-devices": "Общедоступные устройства",
+            "add": "Добавить клиента",
+            "delete": "Удалить клиента",
+            "manage-customer-users": "Управление пользователями клиента",
+            "manage-customer-devices": "Управление устройствами клиента",
+            "manage-customer-dashboards": "Управление дашбордами клиента",
+            "manage-public-devices": "Управление общедоступными устройствами",
+            "manage-public-dashboards": "Управление общедоступными дашбордами",
+            "add-customer-text": "Добавить нового клиента",
+            "no-customers-text": "Клиенты не найдены",
+            "customer-details": "Подробности о клиенте",
+            "delete-customer-title": "Вы точно хотите удалить клиента '{{customerTitle}}'?",
+            "delete-customer-text": "Внимание, после подтверждения клиент и вся связанная с ним информация будут безвозвратно утеряны.",
+            "delete-customers-title": "Вы точно хотите удалить { count, plural, one {1 клиента} other {# клиентов} }?",
+            "delete-customers-action-title": "Удалить { count, plural, one {1 клиента} other {# клиентов} } }",
+            "delete-customers-text": "Внимание, после подтверждения клиенты и вся связанная с ними информация будут безвозвратно утеряны.",
+            "manage-users": "Управление пользователями",
+            "manage-devices": "Управление устройствами",
+            "manage-dashboards": "Управление дашбордами",
+            "title": "Имя",
+            "title-required": "Название обязательно.",
+            "description": "Описание"
+        },
+        "datetime": {
+            "date-from": "Дата с",
+            "time-from": "Время с",
+            "date-to": "Дата до",
+            "time-to": "Время до"
+        },
+        "dashboard": {
+            "dashboard": "Дашборд",
+            "dashboards": "Дашборды",
+            "management": "Управление дашбордами",
+            "view-dashboards": "Просмотреть дашборды",
+            "add": "Добавить дашборд",
+            "assign-dashboard-to-customer": "Прикрепить дашборд(ы) к клиенту",
+            "assign-dashboard-to-customer-text": "Пожалуйста, выберите дашборды, которые нужно прикрепить к клиенту",
+            "assign-to-customer-text": "Пожалуйста, выберите клиента, к которому нужно прикрепить дашборд(ы)",
+            "assign-to-customer": "Прикрепить к клиенту",
+            "unassign-from-customer": "Открепить от клиента",
+            "make-public": "Открыть дашборд для общего доступа",
+            "make-private": "Закрыть дашборд для общего доступа",
+            "no-dashboards-text": "Дашборды не найдены",
+            "no-widgets": "Виджеты не сконфигурированы",
+            "add-widget": "Добавить новый виджет",
+            "title": "Название",
+            "select-widget-title": "Выберите виджет",
+            "select-widget-subtitle": "Список доступных виджетов",
+            "delete": "Удалить дашборд",
+            "title-required": "Название обязательно.",
+            "description": "Описание",
+            "details": "Подробности",
+            "dashboard-details": "Подробности о дашборде",
+            "add-dashboard-text": "Добавить новый дашборд",
+            "assign-dashboards": "Прикрепить дашборды",
+            "assign-new-dashboard": "Прикрепить новый дашборд",
+            "assign-dashboards-text": "Прикрепить { count, plural, 1 {1 дашборд} other {# дашборда} } к клиенту",
+            "delete-dashboards": "Удалить дашборды",
+            "unassign-dashboards": "Открепить дашборды",
+            "unassign-dashboards-action-title": "Открепить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} } от клиента",
+            "delete-dashboard-title": "Вы точно хотите удалить дашборд '{{dashboardTitle}}'?",
+            "delete-dashboard-text": "Внимание, после подтверждения дашборд и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-dashboards-title": "Вы точно хотите удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?",
+            "delete-dashboards-action-title": "Удалить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }",
+            "delete-dashboards-text": "Внимание, после подтверждения дашборды и все связанные с ними данные будут безвозвратно утеряны.",
+            "unassign-dashboard-title": "Вы точно хотите открепить дашборд '{{dashboardTitle}}'?",
+            "unassign-dashboard-text": "После подтверждения дашборд не будет доступен клиенту.",
+            "unassign-dashboard": "Открепить дашборд",
+            "unassign-dashboards-title": "Вы точно хотите открепить { count, plural, one {1 дашборд} few {# дашборда} other {# дашбордов} }?",
+            "unassign-dashboards-text": "После подтверждения выбранные дашборды не будут доступны клиенту.",
+            "public-dashboard-title": "Теперь дашборд общедоступный",
+            "public-dashboard-text": "Теперь ваш дашборд <b>{{dashboardTitle}}</b> доступен всем по <a href='{{publicLink}}' target='_blank'>ссылке</a>:",
+            "public-dashboard-notice": "<b>Примечание:</b> Для получения доступа к данным устройства нужно открыть общий доступ к этому устройству.",
+            "make-private-dashboard-title": "Вы точно хотите закрыть общий доступ к дашборду '{{dashboardTitle}}'?",
+            "make-private-dashboard-text": "После подтверждения дашборд будет закрыт для общего доступа.",
+            "make-private-dashboard": "Закрыть дашборд для общего доступа",
+            "socialshare-text": "'{{dashboardTitle}}' сделано ThingsBoard",
+            "socialshare-title": "'{{dashboardTitle}}' сделано ThingsBoard",
+            "select-dashboard": "Выберите дашборд",
+            "no-dashboards-matching": "Дашборд '{{dashboard}}' не найден.",
+            "dashboard-required": "Дашборд обязателен.",
+            "select-existing": "Выберите существующий дашборд",
+            "create-new": "Создать новый дашборд",
+            "new-dashboard-title": "Новое название дашборда",
+            "open-dashboard": "Открыть дашборд",
+            "set-background": "Установить фон",
+            "background-color": "Фоновый цвет",
+            "background-image": "Фоновая картинка",
+            "background-size-mode": "Размер фона",
+            "no-image": "Картинка не выбрана",
+            "drop-image": "Перетащите картинку или кликните для выбора файла.",
+            "settings": "Настройки",
+            "columns-count": "Количество колонок",
+            "columns-count-required": "Количество колонок обязательно.",
+            "min-columns-count-message": "Минимальное число колонок - 10.",
+            "max-columns-count-message": "Максимальное число колонок - 1000.",
+            "widgets-margins": "Величина отступа между виджетами",
+            "horizontal-margin": "Величина горизонтального отступа",
+            "horizontal-margin-required": "Величина горизонтального отступа обязательна.",
+            "min-horizontal-margin-message": "Минимальная величина горизонтального отступа - 0.",
+            "max-horizontal-margin-message": "Максимальная величина горизонтального отступа - 50.",
+            "vertical-margin": "Величина вертикального отступа",
+            "vertical-margin-required": "Величина вертикального отступа обязательна.",
+            "min-vertical-margin-message": "Минимальная величина вертикального отступа - 0.",
+            "max-vertical-margin-message": "Максимальная величина вертикального отступа - 50.",
+            "display-title": "Показать название дашборда",
+            "title-color": "Цвет названия",
+            "display-device-selection": "Показать выборку устройств",
+            "display-dashboard-timewindow": "Показать временное окно",
+            "display-dashboard-export": "Показать экспорт",
+            "import": "Импортировать дашборд",
+            "export": "Экспортировать дашборд",
+            "export-failed-error": "Не удалось экспортировать дашборд: {{error}}",
+            "create-new-dashboard": "Создать новый дашборд",
+            "dashboard-file": "Файл дашборда",
+            "invalid-dashboard-file-error": "Не удалось импортировать дашборд: неизвестная схема данных дашборда.",
+            "dashboard-import-missing-aliases-title": "Конфигурировать псевдонимы импортированного дашборда",
+            "create-new-widget": "Создать новый виджет",
+            "import-widget": "Импортировать новый виджет",
+            "widget-file": "Файл виджета",
+            "invalid-widget-file-error": "Не удалось импортировать виджет: неизвестная схема данных виджета.",
+            "widget-import-missing-aliases-title": "Конфигурировать псевдонимы импортированного виджета",
+            "open-toolbar": "Открыть панель инструментов",
+            "close-toolbar": "Закрыть панель инструментов",
+            "configuration-error": "Ошибка конфигурирования",
+            "alias-resolution-error-title": "Ошибка конфигурирования псевдонимов дашборда",
+            "invalid-aliases-config": "Не удалось найти устройства, соответствующие фильтру псевдонимов.<br/>" +
+                                      "Пожалуйста, свяжитесь с администратором для устранения этой проблемы.",
+            "select-devices": "Выберите устройства",
+            "assignedToCustomer": "Прикреплен к клиенту",
+            "public": "Общедоступный",
+            "public-link": "Общедоступная ссылка",
+            "copy-public-link": "Скопировать общедоступную ссылку",
+            "public-link-copied-message": "Общедоступная ссылка на дашборд скопирована в буфер обмена"
+        },
+        "datakey": {
+            "settings": "Настройки",
+            "advanced": "Дополнительно",
+            "label": "Метка",
+            "color": "Цвет",
+            "data-generation-func": "Функция генерации данных",
+            "use-data-post-processing-func": "Использовать функцию пост-обработки данных",
+            "configuration": "Конфигурация ключа данных",
+            "timeseries": "Выборка по времени",
+            "attributes": "Атрибуты",
+            "timeseries-required": "Выборка по времени обязательна.",
+            "timeseries-or-attributes-required": "Выборка по времени/атрибуты обязательны.",
+            "function-types": "Тип функции",
+            "function-types-required": "Тип функции обязателен."
+        },
+        "datasource": {
+            "type": "Тип источника данных",
+            "add-datasource-prompt": "Пожалуйста, добавьте источник данных"
+        },
+        "details": {
+            "edit-mode": "Режим редактирования",
+            "toggle-edit-mode": "Режим редактирования"
+        },
+        "device": {
+            "device": "Устройство",
+            "device-required": "Устройство обязательно.",
+            "devices": "Устройства",
+            "management": "Управление устройствами",
+            "view-devices": "Просмотреть устройства",
+            "device-alias": "Псевдоним устройства",
+            "aliases": "Псевдонимы устройства",
+            "no-alias-matching": "'{{alias}}' не найден.",
+            "no-aliases-found": "Псевдонимы не найдены.",
+            "no-key-matching": "'{{key}}' не найден.",
+            "no-keys-found": "Ключи не найдены.",
+            "create-new-alias": "Создать новый!",
+            "create-new-key": "Создать новый!",
+            "duplicate-alias-error": "Найден дублирующийся псевдоним '{{alias}}'.<br>В рамках дашборда псевдонимы устройств должны быть уникальными.",
+            "configure-alias": "Конфигурировать '{{alias}}' псевдоним",
+            "no-devices-matching": "Устройство '{{device}}' не найдено.",
+            "alias": "Псевдоним",
+            "alias-required": "Псевдоним устройства обязателен.",
+            "remove-alias": "Удалить псевдоним устройства",
+            "add-alias": "Добавить псевдоним устройства",
+            "name-starts-with": "Название начинается с",
+            "device-list": "Список устройств",
+            "use-device-name-filter": "Использовать фильтр",
+            "device-list-empty": "Устройства не выбраны.",
+            "device-name-filter-required": "Фильтр названия устройства обязателен.",
+            "device-name-filter-no-device-matched": "Устройства, названия которых начинаются с '{{device}}', не найдены.",
+            "add": "Добавить устройство",
+            "assign-to-customer": "Присвоить клиенту",
+            "assign-device-to-customer": "Присвоить устройство(а) клиенту",
+            "assign-device-to-customer-text": "Пожалуйста, выберите устройства, которые нужно присвоить клиенту",
+            "make-public": "Открыть общий доступ к устройству",
+            "make-private": "Закрыть общий доступ к устройству",
+            "no-devices-text": "Устройства не найдены",
+            "assign-to-customer-text": "Пожалуйста, выберите клиента, которому нужно присвоить устройство(а)",
+            "device-details": "Подробности об устройстве",
+            "add-device-text": "Добавить новое устройство",
+            "credentials": "Учетные данные",
+            "manage-credentials": "Управление учетными данными",
+            "delete": "Удалить устройство",
+            "assign-devices": "Присвоить устройство",
+            "assign-devices-text": "Присвоить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } клиенту",
+            "delete-devices": "Удалить устройства",
+            "unassign-from-customer": "Отменить присвоение клиенту",
+            "unassign-devices": "Отменить присвоение устройств",
+            "unassign-devices-action-title": "Отменить присвоение { count, plural, one {1 устройства} few {# устройств} other {# устройств} } клиенту",
+            "assign-new-device": "Присвоить новое устройство",
+            "make-public-device-title": "Вы точно хотите открыть общий доступ к устройству '{{deviceName}}'?",
+            "make-public-device-text": "После подтверждения устройство и все связанные с ним данные будут общедоступными.",
+            "make-private-device-title": "Вы точно хотите закрыть общий доступ к устройству '{{deviceName}}'",
+            "make-private-device-text": "После подтверждения устройство и все связанные с ним данные будут закрыты для общего доступа.",
+            "view-credentials": "Просмотреть учетные данные",
+            "delete-device-title": "Вы точно хотите удалить устройство '{{deviceName}}'?",
+            "delete-device-text": "Внимание, после подтверждения устройство и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-devices-title": "Вы точно хотите удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} }?",
+            "delete-devices-action-title": "Удалить { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }",
+            "delete-devices-text": "Внимание, после подтверждения выбранные устройства и все связанные с ними данные будут безвозвратно утеряны..",
+            "unassign-device-title": "Вы точно хотите отменить присвоение устройства '{{deviceName}}'?",
+            "unassign-device-text": "После подтверждения устройство будет недоступно клиенту.",
+            "unassign-device": "Отменить присвоение устройства",
+            "unassign-devices-title": "Вы точно хотите отменить присвоение { count, plural, one {1 устройство} few {# устройства} other {# устройств} } }?",
+            "unassign-devices-text": "После подтверждения выбранные устройства будут недоступны клиенту.",
+            "device-credentials": "Учетные данные устройства",
+            "credentials-type": "Тип учетных данных",
+            "access-token": "Токен",
+            "access-token-required": "Токен обязателен.",
+            "access-token-invalid": "Длина токена должна быть от 1 до 20 символов.",
+            "rsa-key": "Открытый ключ RSA",
+            "rsa-key-required": "Открытый ключ RSA обязателен.",
+            "secret": "Секрет",
+            "secret-required": "Секрет обязателен.",
+            "name": "Название",
+            "name-required": "Название обязательно.",
+            "description": "Описание",
+            "events": "События",
+            "details": "Подробности",
+            "copyId": "Копировать идентификатор устройства",
+            "copyAccessToken": "Копировать токен",
+            "idCopiedMessage": "Идентификатор устройства скопирован в буфер обмена",
+            "accessTokenCopiedMessage": "Токен устройства скопирован в буфер обмена",
+            "assignedToCustomer": "Присвоен клиенту",
+            "unable-delete-device-alias-title": "Не удалось удалить псевдоним устройства",
+            "unable-delete-device-alias-text": "Не удалось удалить псевдоним '{{deviceAlias}}' устройства, т.к. он используется следующими виджетами:<br/>{{widgetsList}}",
+            "is-gateway": "Гейтвей",
+            "public": "Общедоступный",
+            "device-public": "Устройство общедоступно"
+        },
+        "dialog": {
+            "close": "Закрыть диалог"
+        },
+        "error": {
+            "unable-to-connect": "Не удалось подключиться к серверу! Пожалуйста, проверьте интернет-соединение.",
+            "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}",
+            "unknown-error": "Неизвестная ошибка"
+        },
+        "event": {
+            "event-type": "Тип события",
+            "type-alarm": "Аварийное оповещение",
+            "type-error": "Ошибка",
+            "type-lc-event": "Событие жизненного цикла",
+            "type-stats": "Статистика",
+            "no-events-prompt": "События не найдены",
+            "error": "Ошибка",
+            "alarm": "Аварийное оповещение",
+            "event-time": "Время возникновения события",
+            "server": "Сервер",
+            "body": "Тело",
+            "method": "Метод",
+            "event": "Событие",
+            "status": "Статус",
+            "success": "Успех",
+            "failed": "Неудача",
+            "messages-processed": "Сообщения обработаны",
+            "errors-occurred": "Возникли ошибки"
+        },
+        "fullscreen": {
+            "expand": "Во весь экран",
+            "exit": "Выйти из полноэкранного режима",
+            "toggle": "Во весь экран",
+            "fullscreen": "Полноэкранный режим"
+        },
+        "function": {
+            "function": "Функция"
+        },
+        "grid": {
+            "delete-item-title": "Вы точно хотите удалить этот объект?",
+            "delete-item-text": "Внимание, после подтверждения объект и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-items-title": "Вы точно хотите удалить { count, plural, one {1 объект} few {# объекта} other {# объектов} }?",
+            "delete-items-action-title": "Удалить { count, plural, one {1 объект} few {# объекта} other {# объектов}",
+            "delete-items-text": "Внимание, после подтверждения выбранные объекты и все связанные с ними данные будут безвозвратно утеряны.",
+            "add-item-text": "Добавить новый объект",
+            "no-items-text": "Объекты не найдены",
+            "item-details": "Подробности об объекте",
+            "delete-item": "Удалить объект",
+            "delete-items": "Удалить объекты",
+            "scroll-to-top": "Прокрутка к началу"
+        },
+        "help": {
+            "goto-help-page": "Перейти к справке"
+        },
+        "home": {
+            "home": "Главная",
+            "profile": "Профиль",
+            "logout": "Выйти из системы",
+            "menu": "Меню",
+            "avatar": "Аватар",
+            "open-user-menu": "Открыть меню пользователя"
+        },
+        "import": {
+            "no-file": "Файл не выбран",
+            "drop-file": "Перетащите JSON файл или кликните для выбора файла."
+        },
+        "item": {
+            "selected": "Выбранные"
+        },
+        "js-func": {
+            "no-return-error": "Функция должна возвращать значение!",
+            "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!"
+        },
+        "legend": {
+            "position": "Расположение легенды",
+            "show-max": "Показать максимальное значение",
+            "show-min": "Показать минимальное значение",
+            "show-avg": "Показать среднее значение",
+            "show-total": "Показать сумму",
+            "settings": "Настройки легенды",
+            "min": "Мин",
+            "max": "Макс",
+            "avg": "Среднее",
+            "total": "Сумма"
+        },
+        "login": {
+            "login": "Войти",
+            "request-password-reset": "Запрос на сброс пароля",
+            "reset-password": "Сбросить пароль",
+            "create-password": "Создать пароль",
+            "passwords-mismatch-error": "Введенные пароли должны быть одинаковыми!",
+            "password-again": "Введите пароль еще раз",
+            "sign-in": "Пожалуйста, войдите в систему",
+            "username": "Имя пользователя (эл. адрес)",
+            "remember-me": "Запомнить меня",
+            "forgot-password": "Забыли пароль?",
+            "password-reset": "Пароль сброшен",
+            "new-password": "Новый пароль",
+            "new-password-again": "Повторите новый пароль",
+            "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!",
+            "email": "Эл. адрес"
+        },
+        "plugin": {
+            "plugins": "Плагины",
+            "delete": "Удалить плагин",
+            "activate": "Активировать плагин",
+            "suspend": "Приостановить плагин",
+            "active": "Активный",
+            "suspended": "Приостановлен",
+            "name": "Название",
+            "name-required": "Название обязательно.",
+            "description": "Описание",
+            "add": "Добавить плагин",
+            "delete-plugin-title": "Вы точно хотите удалить плагин '{{pluginName}}'?",
+            "delete-plugin-text": "Внимание, после подтверждения плагин и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-plugins-title": "Вы точно хотите удалить { count, plural, one {1 плагин} few {# плагина} other {# плагинов} }?",
+            "delete-plugins-action-title": "Удалить { count, plural, one {1 плагин} few {# плагина} other {# плагинов} } }",
+            "delete-plugins-text": "Внимание, после подтверждения выбранные плагины и все связанные с ними данные будут безвозвратно утеряны.",
+            "add-plugin-text": "Добавить новый плагин",
+            "no-plugins-text": "Плагины не найдены",
+            "plugin-details": "Подробности о плагине",
+            "api-token": "API токен",
+            "api-token-required": "API токен обязателен.",
+            "type": "Тип плагина",
+            "type-required": "Тип плагина обязателен.",
+            "configuration": "Настройки плагина",
+            "system": "Системный",
+            "select-plugin": "Выберите плагин",
+            "plugin": "Плагин",
+            "no-plugins-matching": "Плагин '{{plugin}}' не найден.",
+            "plugin-required": "Плагин обязателен.",
+            "plugin-require-match": "Пожалуйста, выберите существующий плагин.",
+            "events": "События",
+            "details": "Подробности",
+            "import": "Импортировать плагин",
+            "export": "Экспортировать плагин",
+            "export-failed-error": "Не удалось экспортировать плагин: {{error}}",
+            "create-new-plugin": "Создать новый плагин",
+            "plugin-file": "Файл плагина",
+            "invalid-plugin-file-error": "Не удалось импортировать плагин: неизвестная схема данных плагина."
+        },
+        "position": {
+            "top": "Верх",
+            "bottom": "Низ",
+            "left": "Левый край",
+            "right": "Правый край"
+        },
+        "profile": {
+            "profile": "Профиль",
+            "change-password": "Изменить пароль",
+            "current-password": "Текущий пароль"
+        },
+        "rule": {
+            "rules": "Правила",
+            "delete": "Удалить правило",
+            "activate": "Активировать правило",
+            "suspend": "Приостановить правило",
+            "active": "Активное",
+            "suspended": "Приостановлены",
+            "name": "Название",
+            "name-required": "Название обязательно.",
+            "description": "Описание",
+            "add": "Добавить правило",
+            "delete-rule-title": "Вы точно хотите удалить правило '{{ruleName}}'?",
+            "delete-rule-text": "Внимание, после подтверждения правило и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-rules-title": "Вы точно хотите удалить { count, plural, one {1 правило} few {# правила} other {# правил} }?",
+            "delete-rules-action-title": "Удалить { count, plural, one {1 правило} few {# правила} other {# правил} }",
+            "delete-rules-text": "Внимание, после подтверждения выбранные правила и все связанные с ними данные будут безвозвратно утеряны.",
+            "add-rule-text": "Добавить новое правило",
+            "no-rules-text": "Правила не найдены",
+            "rule-details": "Подробности о правиле",
+            "filters": "Фильтры",
+            "filter": "Фильтр",
+            "add-filter-prompt": "Пожалуйста, добавьте фильтр",
+            "remove-filter": "Удалить фильтр",
+            "add-filter": "Добавить фильтр",
+            "filter-name": "Название фильтра",
+            "filter-type": "Тип фильтра",
+            "edit-filter": "Редактировать фильтр",
+            "view-filter": "Просмотреть фильтр",
+            "component-name": "Название",
+            "component-name-required": "Название обязательно.",
+            "component-type": "Тип",
+            "component-type-required": "Тип обязателен.",
+            "processor": "Обработчик",
+            "no-processor-configured": "Обработчики не сконфигурированы",
+            "create-processor": "Создать обработчик",
+            "processor-name": "Название обработчика",
+            "processor-type": "Тип обработчика",
+            "plugin-action": "Действие плагина",
+            "action-name": "Название действия",
+            "action-type": "Тип действия",
+            "create-action-prompt": "Пожалуйста, создайте действие",
+            "create-action": "Создать действие",
+            "details": "Подробности",
+            "events": "События",
+            "system": "Системное",
+            "import": "Импортировать правило",
+            "export": "Экспортировать правило",
+            "export-failed-error": "Не удалось экспортировать правило: {{error}}",
+            "create-new-rule": "Создать новое правило",
+            "rule-file": "Файл правила",
+            "invalid-rule-file-error": "Не удалось импортировать правило: неизвестная схема данных правила."
+        },
+        "rule-plugin": {
+            "management": "Управление плагинами и правилами"
+        },
+        "tenant": {
+            "tenants": "Владельцы",
+            "management": "Управление владельцами",
+            "add": "Добавить владельца",
+            "admins": "Администраторы",
+            "manage-tenant-admins": "Управление администраторами владельца",
+            "delete": "Удалить владельца",
+            "add-tenant-text": "Добавить нового владельца",
+            "no-tenants-text": "Владельцы не найдены",
+            "tenant-details": "Подробности об владельце",
+            "delete-tenant-title": "Вы точно хотите удалить владельца '{{tenantTitle}}'?",
+            "delete-tenant-text": "Внимание, после подтверждения владелец и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-tenants-title": "Вы точно хотите удалить { count, plural, one {1 владельца}  other {# владельцев} }?",
+            "delete-tenants-action-title": "Удалить { count, plural, one {1 владельца}  other {# владельцев} }",
+            "delete-tenants-text": "Внимание, после подтверждения выбранные Владельцы и все связанные с ними данные будут безвозвратно утеряны.",
+            "title": "Имя",
+            "title-required": "Имя обязательно.",
+            "description": "Описание"
+        },
+        "timeinterval": {
+            "seconds-interval": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }",
+            "minutes-interval": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }",
+            "hours-interval": "{ hours, plural, one {1 час} few {# часа} other {# часов} }",
+            "days-interval": "{ days, plural, one {1 день} few {# дня} other {# дней} }",
+            "days": "Дни",
+            "hours": "Часы",
+            "minutes": "Минуты",
+            "seconds": "Секунды",
+            "advanced": "Дополнительно"
+        },
+        "timewindow": {
+            "days": "{ days, plural, one {1 день} few {# дня} other {# дней} }",
+            "hours": "{ hours, plural, one {1 час} few {# часа} other {# часов} }",
+            "minutes": "{ minutes, plural, one {1 минута} few {# минуты} other {# минут} }",
+            "seconds": "{ seconds, plural, one {1 секунда} few {# секунды} other {# секунд} }",
+            "realtime": "Режим реального времени",
+            "history": "История",
+            "last-prefix": "Последние",
+            "period": "с {{ startTime }} до {{ endTime }}",
+            "edit": "Изменить временное окно",
+            "date-range": "Диапазон дат",
+            "last": "Последние",
+            "time-period": "Период времени"
+        },
+        "user": {
+            "users": "Пользователи",
+            "customer-users": "Пользователи клиента",
+            "tenant-admins": "Администраторы владельца",
+            "sys-admin": "Системный администратор",
+            "tenant-admin": "Администратор владельца",
+            "customer": "Клиент",
+            "anonymous": "Аноним",
+            "add": "Добавить пользователя",
+            "delete": "Удалить пользователя",
+            "add-user-text": "Добавить нового пользователя",
+            "no-users-text": "Пользователи не найдены",
+            "user-details": "Подробности о пользователе",
+            "delete-user-title": "Вы точно хотите удалить пользователя '{{userEmail}}'?",
+            "delete-user-text": "Внимание, после подтверждения пользователь и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-users-title": "Вы точно хотите удалить { count, plural, one {1 пользователя} other {# пользователей} }?",
+            "delete-users-action-title": "Удалить { count, plural, one {1 пользователя} other {# пользователей} }",
+            "delete-users-text": "Внимание, после подтверждения выбранные пользователи и все связанные с ними данные будут безвозвратно утеряны.",
+            "activation-email-sent-message": "Активационное письмо успешно отправлено!",
+            "resend-activation": "Повторить отправку активационного письма",
+            "email": "Эл. адрес",
+            "email-required": "Эл. адрес обязателен.",
+            "first-name": "Имя",
+            "last-name": "Фамилия",
+            "description": "Описание",
+            "default-dashboard": "Дашборд по умолчанию",
+            "always-fullscreen": "Всегда во весь экран"
+        },
+        "value": {
+            "type": "Тип значения",
+            "string": "Строка",
+            "string-value": "Строковое значение",
+            "integer": "Целое число",
+            "integer-value": "Целочисленное значение",
+            "invalid-integer-value": "Неправильный формат целого числа",
+            "double": "Число двойной точности",
+            "double-value": "Значение двойной точности",
+            "boolean": "Логический тип",
+            "boolean-value": "Логическое значение",
+            "false": "Ложь",
+            "true": "Правда"
+        },
+        "widget": {
+            "widget-library": "Галерея виджетов",
+            "widget-bundle": "Набор виджетов",
+            "select-widgets-bundle": "Выберите набор виджетов",
+            "management": "Управление виджетами",
+            "editor": "Редактор виджетов",
+            "widget-type-not-found": "Ошибка при загрузке конфигурации виджета.<br>Возможно, связанный с ней\n    тип виджета уже удален.",
+            "widget-type-load-error": "Не удалось загрузить виджет по следующим причинам:",
+            "remove": "Удалить виджет",
+            "edit": "Редактировать виджет",
+            "remove-widget-title": "Вы точно хотите удалить виджет '{{widgetTitle}}'?",
+            "remove-widget-text": "Внимание, после подтверждения виджет и все связанные с ним данные будут безвозвратно утеряны.",
+            "timeseries": "Выборка по времени",
+            "latest-values": "Последние значения",
+            "rpc": "Управляющий виджет",
+            "static": "Статический виджет",
+            "select-widget-type": "Выберите тип виджета",
+            "missing-widget-title-error": "Укажите название виджета!",
+            "widget-saved": "Виджет сохранен",
+            "unable-to-save-widget-error": "Не удалось сохранить виджет! Виджет содержит ошибки!",
+            "save": "Сохранить виджет",
+            "saveAs": "Сохранить виджет как",
+            "save-widget-type-as": "Сохранить тип виджета как",
+            "save-widget-type-as-text": "Пожалуйста, введите название виджета и/или укажите целевой набор виджетов",
+            "toggle-fullscreen": "Во весь экран",
+            "run": "Запустить виджет",
+            "title": "Название виджета",
+            "title-required": "Название виджета обязательно.",
+            "type": "Тип виджета",
+            "resources": "Ресурсы",
+            "resource-url": "JavaScript/CSS URL",
+            "remove-resource": "Удалить ресурс",
+            "add-resource": "Добавить ресурс",
+            "html": "HTML",
+            "tidy": "Форматировать",
+            "css": "CSS",
+            "settings-schema": "Схема конфигурации",
+            "datakey-settings-schema": "Схема конфигурации ключа данных",
+            "javascript": "Javascript",
+            "remove-widget-type-title": "Вы точно хотите удалить виджет '{{widgetName}}'?",
+            "remove-widget-type-text": "Внимание, после подтверждения тип виджета и все связанные с ним данные будут безвозвратно утеряны.",
+            "remove-widget-type": "Удалить тип виджета",
+            "add-widget-type": "Добавить новый тип виджета",
+            "widget-type-load-failed-error": "Не удалось загрузить тип виджета!",
+            "widget-template-load-failed-error": "Не удалось загрузить шаблон виджета!",
+            "add": "Добавить виджет",
+            "undo": "Откатить изменения в виджете",
+            "export": "Экспортировать виджет"
+        },
+        "widgets-bundle": {
+            "current": "Текущий набор",
+            "widgets-bundles": "Наборы виджетов",
+            "add": "Добавить набор виджетов",
+            "delete": "Удалить набор виджетов",
+            "title": "Название",
+            "title-required": "Название обязательно.",
+            "add-widgets-bundle-text": "Добавить новый набор виджетов",
+            "no-widgets-bundles-text": "Наборы виджетов не найдены",
+            "empty": "Пустой набор виджетов",
+            "details": "Подробности",
+            "widgets-bundle-details": "Подробности о наборе виджетов",
+            "delete-widgets-bundle-title": "Вы точно хотите удалить набор виджетов '{{widgetsBundleTitle}}'?",
+            "delete-widgets-bundle-text": "Внимание, после подтверждения набор виджетов и все связанные с ним данные будут безвозвратно утеряны.",
+            "delete-widgets-bundles-title": "Вы точно хотите удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }?",
+            "delete-widgets-bundles-action-title": "Удалить { count, plural, one {1 набор виджетов} few {# набора виджетов} other {# наборов виджетов} }",
+            "delete-widgets-bundles-text": "Внимание, после подтверждения выбранные наборы виджетов и все связанные с ними данные будут безвозвратно утеряны..",
+            "no-widgets-bundles-matching": "Набор виджетов '{{widgetsBundle}}' не найден.",
+            "widgets-bundle-required": "Набор виджетов обязателен.",
+            "system": "Системный",
+            "import": "Импортировать набор виджетов",
+            "export": "Экспортировать набор виджетов",
+            "export-failed-error": "Не удалось экспортировать набор виджетов: {{error}}",
+            "create-new-widgets-bundle": "Создать новый набор виджетов",
+            "widgets-bundle-file": "Файл набора виджетов",
+            "invalid-widgets-bundle-file-error": "Не удалось импортировать набор виджетов: неизвестная схема данных набора виджетов."
+        },
+        "widget-config": {
+            "data": "Данные",
+            "settings": "Настройки",
+            "advanced": "Дополнительно",
+            "title": "Название",
+            "general-settings": "Общие настройки",
+            "display-title": "Показать название",
+            "drop-shadow": "Тень",
+            "enable-fullscreen": "Во весь экран",
+            "background-color": "Цвет фона",
+            "text-color": "Цвет текста",
+            "padding": "Отступ",
+            "title-style": "Стиль названия",
+            "mobile-mode-settings": "Настройки мобильного режима",
+            "order": "Порядок",
+            "height": "Высота",
+            "units": "Специальный символ после значения",
+            "decimals": "Количество цифр после запятой",
+            "timewindow": "Временное окно",
+            "use-dashboard-timewindow": "Использовать временное окно дашборда",
+            "display-legend": "Показать легенду",
+            "datasources": "Источники данных",
+            "datasource-type": "Тип",
+            "datasource-parameters": "Параметры",
+            "remove-datasource": "Удалить источник данных",
+            "add-datasource": "Добавить источник данных",
+            "target-device": "Целевое устройство"
+        },
+        "widget-type": {
+            "import": "Импортировать тип виджета",
+            "export": "Экспортировать тип виджета",
+            "export-failed-error": "Не удалось экспортировать тип виджета: {{error}}",
+            "create-new-widget-type": "Создать новый тип виджета",
+            "widget-type-file": "Файл типа виджета",
+            "invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета."
+        },
+        "language": {
+            "language": "Язык",
+            "en_US": "Английский",
+            "ko_KR": "Корейский",
+            "zh_CN": "Китайский",
+            "ru_RU": "Русский",
+            "es_ES": "испанский"
+
+        }
+    };
+    angular.extend(locales, {'ru_RU': ru_RU});
+}
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant-zh.js b/ui/src/app/locale/locale.constant-zh.js
new file mode 100644
index 0000000..56e4d57
--- /dev/null
+++ b/ui/src/app/locale/locale.constant-zh.js
@@ -0,0 +1,820 @@
+/*
+ * 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 function addLocaleChinese(locales) {
+    var zh_CN = {
+        "access" : {
+            "unauthorized" : "未授权",
+            "unauthorized-access" : "未授权访问",
+            "unauthorized-access-text" : "您需要登陆才能访问这个资源!",
+            "access-forbidden" : "禁止访问",
+            "access-forbidden-text" : "您没有访问此位置的权限<br/>如果您仍希望访问此位置,请尝试使用其他用户登录。",
+            "refresh-token-expired" : "会话已过期",
+            "refresh-token-failed" : "无法刷新会话"
+        },
+        "action" : {
+            "activate" : "激活",
+            "suspend" : "暂停",
+            "save" : "保存",
+            "saveAs" : "另存为",
+            "cancel" : "取消",
+            "ok" : "确定",
+            "delete" : "删除",
+            "add" : "添加",
+            "yes" : "是",
+            "no" : "否",
+            "update" : "更新",
+            "remove" : "移除",
+            "search" : "查询",
+            "assign" : "分配",
+            "unassign" : "取消分配",
+            "share" : "分享",
+            "make-private" : "私有",
+            "apply" : "应用",
+            "apply-changes" : "应用更改",
+            "edit-mode" : "编辑模式",
+            "enter-edit-mode" : "进入编辑模式",
+            "decline-changes" : "拒绝变更",
+            "close" : "关闭",
+            "back" : "后退",
+            "run" : "运行",
+            "sign-in" : "登录!",
+            "edit" : "编辑",
+            "view" : "查看",
+            "create" : "创建",
+            "drag" : "拖拽",
+            "refresh" : "刷新",
+            "undo" : "撤销",
+            "copy" : "复制",
+            "paste" : "粘贴",
+            "import" : "导入",
+            "export" : "导出",
+            "share-via" : "通过 {{provider}}分享"
+        },
+        "aggregation" : {
+            "aggregation" : "聚合",
+            "function" : "数据聚合功能",
+            "limit" : "最大值",
+            "group-interval" : "分组间隔",
+            "min" : "最少值",
+            "max" : "最大值",
+            "avg" : "平均值",
+            "sum" : "求和",
+            "count" : "计数",
+            "none" : "空"
+        },
+        "admin" : {
+            "general" : "常规",
+            "general-settings" : "常规设置",
+            "outgoing-mail" : "发送邮件",
+            "outgoing-mail-settings" : "发送邮件设置",
+            "system-settings" : "系统设置",
+            "test-mail-sent" : "测试邮件发送成功!",
+            "base-url" : "基本URL",
+            "base-url-required" : "基本URL是必须的。",
+            "mail-from" : "邮件来自",
+            "mail-from-required" : "邮件发件人是必须的。",
+            "smtp-protocol" : "SMTP协议",
+            "smtp-host" : "SMTP主机",
+            "smtp-host-required" : "SMTP主机是必须的。",
+            "smtp-port" : "SMTP端口",
+            "smtp-port-required" : "您必须提供一个smtp端口。",
+            "smtp-port-invalid" : "这看起来不是有效的smtp端口。",
+            "timeout-msec" : "超时(ms)",
+            "timeout-required" : "超时是必须的。",
+            "timeout-invalid" : "这看起来不像有效的超时值。",
+            "enable-tls" : "启用TLS",
+            "send-test-mail" : "发送测试邮件"
+        },
+        "attribute" : {
+            "attributes" : "属性",
+            "latest-telemetry" : "最新遥测",
+            "attributes-scope" : "设备属性范围",
+            "scope-latest-telemetry" : "最新遥测",
+            "scope-client" : "客户端属性",
+            "scope-server" : "服务端属性",
+            "scope-shared" : "共享属性",
+            "add" : "添加属性",
+            "key" : "键",
+            "key-required" : "属性键是必需的。",
+            "value" : "值",
+            "value-required" : "属性值是必需的。",
+            "delete-attributes-title" : "您确定要删除 { count, select, 1 {1 attribute} other {# attributes} }吗?",
+            "delete-attributes-text" : "注意,确认后所有选中的属性都会被删除。",
+            "delete-attributes" : "删除属性",
+            "enter-attribute-value" : "输入属性值",
+            "show-on-widget" : "在小部件上显示",
+            "widget-mode" : "小部件模式",
+            "next-widget" : "下一个小部件",
+            "prev-widget" : "上一个小部件",
+            "add-to-dashboard" : "添加到仪表板",
+            "add-widget-to-dashboard" : "将小部件添加到仪表板",
+            "selected-attributes" : "{ count, select, 1 {1 attribute} other {# attributes} } 被选中",
+            "selected-telemetry" : "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } 被选中"
+        },
+        "confirm-on-exit" : {
+            "message" : "您有未保存的更改。确定要离开此页吗?",
+            "html-message" : "您有未保存的更改。<br/> 确定要离开此页面吗?",
+            "title" : "未保存的更改"
+        },
+        "contact" : {
+            "country" : "国家",
+            "city" : "城市",
+            "state" : "州",
+            "postal-code" : "邮政编码",
+            "postal-code-invalid" : "只允许数字。",
+            "address" : "地址",
+            "address2" : "地址2",
+            "phone" : "手机",
+            "email" : "邮箱",
+            "no-address" : "无地址"
+        },
+        "common" : {
+            "username" : "用户名",
+            "password" : "密码",
+            "enter-username" : "输入用户名",
+            "enter-password" : "输入密码",
+            "enter-search" : "输入检索条件"
+        },
+        "customer" : {
+            "customers" : "客户",
+            "management" : "客户管理",
+            "dashboard" : "客户仪表板",
+            "dashboards" : "客户仪表板",
+            "devices" : "客户设备",
+            "public-dashboards" : "公共仪表板",
+            "public-devices" : "公共设备",
+            "add" : "添加客户",
+            "delete" : "删除客户",
+            "manage-customer-users" : "管理客户用户",
+            "manage-customer-devices" : "管理客户设备",
+            "manage-customer-dashboards" : "管理客户仪表板",
+            "manage-public-devices" : "管理公共设备",
+            "manage-public-dashboards" : "管理公共仪表板",
+            "add-customer-text" : "添加新客户",
+            "no-customers-text" : "没有找到客户",
+            "customer-details" : "客户详情",
+            "delete-customer-title" : "您确定要删除客户'{{customerTitle}}'吗?",
+            "delete-customer-text" : "小心!确认后,客户及其所有相关数据将不可恢复。",
+            "delete-customers-title" : "您确定要删除 { count, select, 1 {1 customer} other {# customers} }?吗",
+            "delete-customers-action-title" : "删除 { count, select, 1 {1 customer} other {# customers} }",
+            "delete-customers-text" : "小心!确认后,所有选定的客户将被删除,所有相关数据将不可恢复。",
+            "manage-users" : "管理用户",
+            "manage-devices" : "管理设备",
+            "manage-dashboards" : "管理仪表板",
+            "title" : "标题",
+            "title-required" : "需要标题。",
+            "description" : "描述"
+        },
+        "datetime" : {
+            "date-from" : "日期从",
+            "time-from" : "时间从",
+            "date-to" : "日期到",
+            "time-to" : "时间到"
+        },
+        "dashboard" : {
+            "dashboard" : "仪表板",
+            "dashboards" : "仪表板库",
+            "management" : "仪表板管理",
+            "view-dashboards" : "查看仪表板",
+            "add" : "添加仪表板",
+            "assign-dashboard-to-customer" : "将仪表板分配给客户",
+            "assign-dashboard-to-customer-text" : "请选择要分配给客户的仪表板",
+            "assign-to-customer-text" : "请选择客户分配仪表板",
+            "assign-to-customer" : "分配给客户",
+            "unassign-from-customer" : "取消分配客户",
+            "make-public" : "使仪表板公有",
+            "make-private" : "使仪表板私有",
+            "no-dashboards-text" : "没有找到仪表板",
+            "no-widgets" : "没有配置小部件",
+            "add-widget" : "添加新的小部件",
+            "title" : "标题",
+            "select-widget-title" : "选择小部件",
+            "select-widget-subtitle" : "可用的小部件类型列表",
+            "delete" : "删除仪表板",
+            "title-required" : "需要标题。",
+            "description" : "描述",
+            "details" : "详情",
+            "dashboard-details" : "仪表板详情",
+            "add-dashboard-text" : "添加新的仪表板",
+            "assign-dashboards" : "分配仪表板",
+            "assign-new-dashboard" : "分配新的仪表板",
+            "assign-dashboards-text" : "分配 { count, select, 1 {1 dashboard} other {# dashboards} } 给客户",
+            "delete-dashboards" : "删除仪表板",
+            "unassign-dashboards" : "取消分配仪表板",
+            "unassign-dashboards-action-title" : "取消分配 { count, select, 1 {1 dashboard} other {# dashboards} } from customer",
+            "delete-dashboard-title" : "您确定要删除仪表板 '{{dashboardTitle}}'吗?",
+            "delete-dashboard-text" : "小心!确认后仪表板及其所有相关数据将不可恢复。",
+            "delete-dashboards-title" : "你确定你要删除 { count, select, 1 {1 dashboard} other {# dashboards} }吗?",
+            "delete-dashboards-action-title" : "删除 { count, select, 1 {1 dashboard} other {# dashboards} }",
+            "delete-dashboards-text" : "小心!确认后所有选定的仪表板将被删除,所有相关数据将不可恢复。",
+            "unassign-dashboard-title" : "您确定要取消分配仪表板 '{{dashboardTitle}}'吗?",
+            "unassign-dashboard-text" : "确认后,面板将被取消分配,客户将无法访问。",
+            "unassign-dashboard" : "取消分配仪表板",
+            "unassign-dashboards-title" : "您确定要取消分配仪表板 { count, select, 1 {1 dashboard} other {# dashboards} } 吗?",
+            "unassign-dashboards-text" : "确认后,所有选定的仪表板将被取消分配,客户将无法访问。",
+            "public-dashboard-title" : "仪表板现已公布",
+            "public-dashboard-text" : "你的仪表板 <b>{{dashboardTitle}}</b> 已被公开,可通过如下 <a href='{{publicLink}}' target='_blank'>链接</a>访问:",
+            "public-dashboard-notice" : "<b>提示:</b> 不要忘记将相关设备公开以访问其数据。",
+            "make-private-dashboard-title" : "您确定要使仪表板 '{{dashboardTitle}}' 私有吗?",
+            "make-private-dashboard-text" : "确认后,仪表板将被私有,不能被其他人访问。",
+            "make-private-dashboard" : "仪表板私有",
+            "socialshare-text" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
+            "socialshare-title" : "'{{dashboardTitle}}' 由ThingsBoard提供支持",
+            "select-dashboard" : "选择仪表板",
+            "no-dashboards-matching" : "找不到符合 '{{dashboard}}' 的仪表板。",
+            "dashboard-required" : "仪表板是必需的。",
+            "select-existing" : "选择现有仪表板",
+            "create-new" : "创建新的仪表板",
+            "new-dashboard-title" : "新仪表板标题",
+            "open-dashboard" : "打开仪表板",
+            "set-background" : "设置背景",
+            "background-color" : "背景颜色",
+            "background-image" : "背景图片",
+            "background-size-mode" : "背景大小模式",
+            "no-image" : "无图像选择",
+            "drop-image" : "拖拽图像或单击以选择要上传的文件。",
+            "settings" : "设置",
+            "columns-count" : "列数",
+            "columns-count-required" : "需要列数。",
+            "min-columns-count-message" : "只允许最少10列",
+            "max-columns-count-message" : "只允许最多1000列",
+            "widgets-margins" : "部件间边距",
+            "horizontal-margin" : "水平边距",
+            "horizontal-margin-required" : "需要水平边距值。",
+            "min-horizontal-margin-message" : "只允许0作为最小水平边距值。",
+            "max-horizontal-margin-message" : "只允许50作为最大水平边距值。",
+            "vertical-margin" : "垂直边距",
+            "vertical-margin-required" : "需要垂直边距值。",
+            "min-vertical-margin-message" : "只允许0作为最小垂直边距值。",
+            "max-vertical-margin-message" : "只允许50作为最大垂直边距值。",
+            "display-title" : "显示仪表板标题",
+            "title-color" : "标题颜色",
+            "display-device-selection" : "显示设备选择",
+            "display-dashboard-timewindow" : "显示时间窗口",
+            "display-dashboard-export" : "显示导出",
+            "import" : "导入仪表板",
+            "export" : "导出仪表板",
+            "export-failed-error" : "无法导出仪表板: {{error}}",
+            "create-new-dashboard" : "创建新的仪表板",
+            "dashboard-file" : "仪表板文件",
+            "invalid-dashboard-file-error" : "无法导入仪表板: 仪表板数据结构无效。",
+            "dashboard-import-missing-aliases-title" : "配置导入仪表板使用的别名",
+            "create-new-widget" : "创建新小部件",
+            "import-widget" : "导入小部件",
+            "widget-file" : "小部件文件",
+            "invalid-widget-file-error" : "无法导入窗口小部件: 窗口小部件数据结构无效。",
+            "widget-import-missing-aliases-title" : "配置导入的窗口小部件使用的别名",
+            "open-toolbar" : "打开仪表板工具栏",
+            "close-toolbar" : "关闭工具栏",
+            "configuration-error" : "配置错误",
+            "alias-resolution-error-title" : "仪表板别名配置错误",
+            "invalid-aliases-config" : "无法找到与某些别名过滤器匹配的任何设备。<br/>" +
+                "请联系您的管理员以解决此问题。",
+            "select-devices" : "选择设备",
+            "assignedToCustomer" : "分配给客户",
+            "public" : "公共",
+            "public-link" : "公共链接",
+            "copy-public-link" : "复制公共链接",
+            "public-link-copied-message" : "仪表板的公共链接已被复制到剪贴板"
+        },
+        "datakey" : {
+            "settings": "设置",
+            "advanced": "高级",
+            "label": "标签",
+            "color": "颜色",
+            "data-generation-func": "数据生成功能",
+            "use-data-post-processing-func": "使用数据后处理功能",
+            "configuration": "数据键配置",
+            "timeseries": "时间序列",
+            "attributes": "属性",
+            "timeseries-required": "需要设备时间序列。",
+            "timeseries-or-attributes-required": "设备时间/属性是必需的。",
+            "function-types": "函数类型",
+            "function-types-required": "需要函数类型。"
+        },
+        "datasource" : {
+            "type": "数据源类型",
+            "add-datasource-prompt": "请添加数据源"
+        },
+        "details" : {
+            "edit-mode": "编辑模式",
+            "toggle-edit-mode": "切换编辑模式"
+        },
+        "device" : {
+            "device": "设备",
+            "device-required": "设备是必需的",
+            "devices": "设备",
+            "management": "设备管理",
+            "view-devices": "查看设备",
+            "device-alias": "设备别名",
+            "aliases": "设备别名",
+            "no-alias-matching" : "'{{alias}}' 没有找到。",
+            "no-aliases-found" : "找不到别名。",
+            "no-key-matching" : "'{{key}}' 没有找到。",
+            "no-keys-found" : "找不到密钥。",
+            "create-new-alias": "创建一个新的!",
+            "create-new-key": "创建一个新的!",
+            "duplicate-alias-error" : "找到重复别名 '{{alias}}'。 <br> 设备别名必须是唯一的。",
+            "configure-alias" : "配置 '{{alias}}' 别名",
+            "no-devices-matching" : "找不到与 '{{device}}' 匹配的设备。",
+            "alias" : "别名",
+            "alias-required" : "需要设备别名。",
+            "remove-alias": "删除设备别名",
+            "add-alias": "添加设备别名",
+            "name-starts-with" : "名称前缀",
+            "device-list" : "设备列表",
+            "use-device-name-filter" : "使用过滤器",
+            "device-list-empty" : "没有被选中的设备",
+            "device-name-filter-required" : "设备名称过滤器是必需得。",
+            "device-name-filter-no-device-matched" : "找不到以'{{device}}' 开头的设备。",
+            "add" : "添加设备",
+            "assign-to-customer": "分配给客户",
+            "assign-device-to-customer": "将设备分配给客户",
+            "assign-device-to-customer-text": "请选择要分配给客户的设备",
+            "make-public" : "公有",
+            "make-private" : "私有",
+            "no-devices-text": "找不到设备",
+            "assign-to-customer-text": "请选择客户分配设备",
+            "device-details": "设备详细信息",
+            "add-device-text": "添加新设备",
+            "credentials": "凭据",
+            "manage-credentials": "管理凭据",
+            "delete": "删除设备",
+            "assign-devices": "分配设备",
+            "assign-devices-text": "将{count,select,1 {1 device} other {# devices}}分配给客户",
+            "delete-devices": "删除设备",
+            "unassign-from-customer": "取消分配客户",
+            "unassign-devices": "取消分配设备",
+            "unassign-devices-action-title": "从客户处取消分配{count,select,1 {1 device} other {# devices}}",
+            "assign-new-device": "分配新设备",
+            "make-public-device-title" : "您确定要将设备 '{{deviceName}}' 设为公开吗?",
+            "make-public-device-text" : "确认后,设备及其所有数据将被公开并可被其他人访问。",
+            "make-private-device-title" : "您确定要将设备 '{{deviceName}}' 设为私有吗?",
+            "make-private-device-text" : "确认后,设备及其所有数据将被私有化,不被其他人访问。",
+            "view-credentials": "查看凭据",
+            "delete-device-title": "您确定要删除设备的{{deviceName}}吗?",
+            "delete-device-text": "小心!确认后设备及其所有相关数据将不可恢复。",
+            "delete-devices-title": "您确定要删除{count,select,1 {1 device} other {# devices}} 吗?",
+            "delete-devices-action-title": "删除 {count,select,1 {1 device} other {# devices}}",
+            "delete-devices-text": "小心!确认后所有选定的设备将被删除,所有相关数据将不可恢复。",
+            "unassign-device-title": "您确定要取消分配设备 '{{deviceName}}'?",
+            "unassign-device-text": "确认后,设备将被取消分配,客户将无法访问。",
+            "unassign-device": "取消分配设备",
+            "unassign-devices-title": "您确定要取消分配{count,select,1 {1 device} other {# devices}} 吗?",
+            "unassign-devices-text": "确认后,所有选定的设备将被取消分配,并且客户将无法访问。",
+            "device-credentials": "设备凭据",
+            "credentials-type": "凭据类型",
+            "access-token": "访问令牌",
+            "access-token-required": "需要访问令牌",
+            "access-token-invalid": "访问令牌长度必须为1到20个字符。",
+            "rsa-key": "RSA公钥",
+            "rsa-key-required": "需要RSA公钥",
+            "secret": "密钥",
+            "secret-required": "密钥是必需的",
+            "name": "名称",
+            "name-required": "名称是必需的。",
+            "description": "说明",
+            "events": "事件",
+            "details": "详细信息",
+            "copyId": "复制设备ID",
+            "copyAccessToken": "复制访问令牌",
+            "idCopiedMessage": "设备ID已复制到剪贴板",
+            "accessTokenCopiedMessage": "设备访问令牌已复制到剪贴板",
+            "assignedToCustomer": "分配给客户",
+            "unable-delete-device-alias-title": "无法删除设备别名",
+            "unable-delete-device-alias-text": "设备别名 '{{deviceAlias}}' 不能够被删除,因为它被下列部件所使用: <br/> {{widgetsList}}",
+            "is-gateway": "是网关",
+            "public" : "公共",
+            "device-public" : "设备是公共的"
+        },
+        "dialog" : {
+            "close" : "关闭对话框"
+        },
+        "error" : {
+            "unable-to-connect": "无法连接到服务器!请检查您的互联网连接。",
+            "unhandled-error-code": "未处理的错误代码: {{errorCode}}",
+            "unknown-error": "未知错误"
+        },
+        "event" : {
+            "event-type": "事件类型",
+            "type-alarm": "报警",
+            "type-error": "错误",
+            "type-lc-event": "生命周期事件",
+            "type-stats": "类型统计",
+            "no-events-prompt": "找不到事件",
+            "error": "错误",
+            "alarm": "报警",
+            "event-time": "事件时间",
+            "server": "服务器",
+            "body": "整体",
+            "method": "方法",
+            "event": "事件",
+            "status": "状态",
+            "success": "成功",
+            "failed": "失败",
+            "messages-processed": "消息处理",
+            "errors-occurred": "错误发生"
+        },
+        "fullscreen" : {
+            "expand": "展开到全屏",
+            "exit": "退出全屏",
+            "toggle": "切换全屏模式",
+            "fullscreen": "全屏"
+        },
+        "function" : {
+            "function" : "函数"
+        },
+        "grid" : {
+            "delete-item-title": "您确定要删除此项吗?",
+            "delete-item-text": "注意,确认后此项及其所有相关数据将变得不可恢复。",
+            "delete-items-title" : "你确定你要删除 { count, select, 1 {1 item} other {# items} }吗?",
+            "delete-items-action-title" : "删除 { count, select, 1 {1 item} other {# items} }",
+            "delete-items-text": "注意,确认后所有选择的项目将被删除,所有相关数据将不可恢复。",
+            "add-item-text": "添加新项目",
+            "no-items-text": "没有找到项目",
+            "item-details": "项目详细信息",
+            "delete-item": "删除项目",
+            "delete-items": "删除项目",
+            "scroll-to-top": "滚动到顶部"
+        },
+        "help" : {
+            "goto-help-page" : "转到帮助页面"
+        },
+        "home" : {
+            "home": "首页",
+            "profile": "属性",
+            "logout": "注销",
+            "menu": "菜单",
+            "avatar": "头像",
+            "open-user-menu": "打开用户菜单"
+        },
+        "import" : {
+            "no-file" : "没有选择文件",
+            "drop-file" : "拖动一个JSON文件或者单击以选择要上传的文件。"
+        },
+        "item" : {
+            "selected" : "选择"
+        },
+        "js-func" : {
+            "no-return-error": "函数必须返回值!",
+            "return-type-mismatch": "函数必须返回 '{{type}}' 类型的值!"
+        },
+        "legend" : {
+            "position" : "图例位置",
+            "show-max" : "显示最大值",
+            "show-min" : "显示最小值",
+            "show-avg" : "显示平均值",
+            "show-total" : "显示总数",
+            "settings" : "图例设置",
+            "min" : "最小值",
+            "max" : "最大值",
+            "avg" : "平均值",
+            "total" : "总数"
+        },
+        "login" : {
+            "login": "登录",
+            "request-password-reset": "请求密码重置",
+            "reset-password": "重置密码",
+            "create-password": "创建密码",
+            "passwords-mismatch-error": "输入的密码必须相同!",
+            "password-again": "再次输入密码",
+            "sign-in": "登录 ",
+            "username": "用户名(电子邮件)",
+            "remember-me": "记住我",
+            "forgot-password": "忘记密码?",
+            "password-reset": "密码重置",
+            "new-password": "新密码",
+            "new-password-again": "再次输入新密码",
+            "password-link-sent-message": "密码重置链接已成功发送!",
+            "email": "电子邮件"
+        },
+        "plugin" : {
+            "plugins" : "插件",
+            "delete" : "删除插件",
+            "activate" : "激活插件",
+            "suspend" : "暂停插件",
+            "active" : "激活",
+            "suspended" : "暂停",
+            "name" : "名称",
+            "name-required" : "名称是必填项。",
+            "description" : "描述",
+            "add" : "添加插件",
+            "delete-plugin-title" : "你确定要删除插件 '{{pluginName}}' 吗?",
+            "delete-plugin-text" : "小心!确认后,插件和所有相关数据将不可恢复。",
+            "delete-plugins-title" : "你确定你要删除 { count, select, 1 {1 plugin} other {# plugins} } 吗?",
+            "delete-plugins-action-title" : "删除 { count, select, 1 {1 plugin} other {# plugins} }",
+            "delete-plugins-text" : "小心!确认后,所有选定的插件将被删除,所有相关数据将不可恢复。",
+            "add-plugin-text" : "添加新的插件",
+            "no-plugins-text" : "没有找到插件",
+            "plugin-details" : "插件详细信息",
+            "api-token" : "API令牌",
+            "api-token-required" : "API令牌是必需的。",
+            "type" : "插件类型",
+            "type-required" : "插件类型是必需的。",
+            "configuration" : "插件配置",
+            "system" : "系统",
+            "select-plugin" : "选择插件",
+            "plugin" : "插件",
+            "no-plugins-matching" : "没有找到匹配'{{plugin}}'的插件。",
+            "plugin-required" : "插件是必需的。",
+            "plugin-require-match" : "请选择一个现有的插件。",
+            "events" : "事件",
+            "details" : "详情",
+            "import" : "导入插件",
+            "export" : "导出插件",
+            "export-failed-error" : "无法导出插件:{{error}}",
+            "create-new-plugin" : "创建新的插件",
+            "plugin-file" : "插件文件",
+            "invalid-plugin-file-error" : "无法导入插件:插件数据结构无效。"
+        },
+        "position" : {
+            "top" : "顶部",
+            "bottom" : "底部",
+            "left" : "左侧",
+            "right" : "右侧"
+        },
+        "profile" : {
+            "profile": "属性",
+            "change-password": "更改密码",
+            "current-password": "当前密码"
+        },
+        "rule" : {
+            "rules" : "规则",
+            "delete" : "删除规则",
+            "activate" : "激活规则",
+            "suspend" : "暂停规则",
+            "active" : "激活",
+            "suspended" : "暂停",
+            "name" : "名称",
+            "name-required" : "名称是必填项。",
+            "description" : "描述",
+            "add" : "添加规则",
+            "delete-rule-title" : "您确定要删除规则'{{ruleName}}'吗?",
+            "delete-rule-text" : "小心!确认后,规则和所有相关数据将不可恢复。",
+            "delete-rules-title" : "你确定要删除 {count, select, 1 {1 rule} other {# rules}} 吗?",
+            "delete-rules-action-title" : "删除 { count, select, 1 {1 rule} other {# rules} }",
+            "delete-rules-text" : "小心!确认后,所有选定的规则将被删除,所有相关数据将不可恢复。",
+            "add-rule-text" : "添加新规则",
+            "no-rules-text" : "没有找到规则",
+            "rule-details" : "规则详情",
+            "filters" : "过滤器",
+            "filter" : "过滤器",
+            "add-filter-prompt" : "请添加过滤器",
+            "remove-filter" : "删除过滤器",
+            "add-filter" : "添加过滤器",
+            "filter-name" : "过滤器名称",
+            "filter-type" : "过滤器类型",
+            "edit-filter" : "编辑过滤器",
+            "view-filter" : "查看过滤器",
+            "component-name" : "名称",
+            "component-name-required" : "名称是必填项。",
+            "component-type" : "类型",
+            "component-type-required" : "类型是必填项。",
+            "processor" : "处理器",
+            "no-processor-configured" : "未配置处理器",
+            "create-processor" : "创建处理器",
+            "processor-name" : "处理器名称",
+            "processor-type" : "处理器类型",
+            "plugin-action" : "插件动作",
+            "action-name" : "动作名称",
+            "action-type" : "动作类型",
+            "create-action-prompt" : "请创建动作",
+            "create-action" : "创建动作",
+            "details" : "详情",
+            "events" : "事件",
+            "system" : "系统",
+            "import" : "导入规则",
+            "export" : "导出规则",
+            "export-failed-error" : "无法导出规则:{{error}}",
+            "create-new-rule" : "创建新规则",
+            "rule-file" : "规则文件",
+            "invalid-rule-file-error" : "无法导入规则:规则数据结构无效。"
+        },
+        "rule-plugin" : {
+            "management" : "规则和插件管理"
+        },
+        "tenant" : {
+            "tenants" : "租户",
+            "management" : "租户管理",
+            "add" : "添加租户",
+            "admins" : "管理员",
+            "manage-tenant-admins" : "管理租户管理员",
+            "delete" : "删除租户",
+            "add-tenant-text" : "添加新租户",
+            "no-tenants-text" : "没有找到租户",
+            "tenant-details" : "租客详情",
+            "delete-tenant-title" : "您确定要删除租户'{{tenantTitle}}'吗?",
+            "delete-tenant-text" : "小心!确认后,租户和所有相关数据将不可恢复。",
+            "delete-tenants-title" : "您确定要删除 {count,select,1 {1 tenant} other {# tenants}} 吗?",
+            "delete-tenants-action-title" : "删除 { count, select, 1 {1 tenant} other {# tenants} }",
+            "delete-tenants-text" : "小心!确认后,所有选定的租户将被删除,所有相关数据将不可恢复。",
+            "title" : "标题",
+            "title-required" : "标题是必填项。",
+            "description" : "描述"
+        },
+        "timeinterval" : {
+            "seconds-interval" : "{ seconds, select, 1 {1 second} other {# seconds} }",
+            "minutes-interval" : "{ minutes, select, 1 {1 minute} other {# minutes} }",
+            "hours-interval" : "{ hours, select, 1 {1 hour} other {# hours} }",
+            "days-interval" : "{ days, select, 1 {1 day} other {# days} }",
+            "days" : "天",
+            "hours" : "时",
+            "minutes" : "分",
+            "seconds" : "秒",
+            "advanced" : "高级"
+        },
+        "timewindow" : {
+            "days" : "{ days, select, 1 { day } other {# days } }",
+            "hours" : "{ hours, select, 0 { hour } 1 {1 hour } other {# hours } }",
+            "minutes" : "{ minutes, select, 0 { minute } 1 {1 minute } other {# minutes } }",
+            "seconds" : "{ seconds, select, 0 { second } 1 {1 second } other {# seconds } }",
+            "realtime" : "实时",
+            "history" : "历史",
+            "last-prefix" : "最后",
+            "period" : "从 {{ startTime }} 到 {{ endTime }}",
+            "edit" : "编辑时间窗口",
+            "date-range" : "日期范围",
+            "last" : "最后",
+            "time-period" : "时间段"
+        },
+        "user" : {
+            "users" : "用户",
+            "customer-users" : "客户用户",
+            "tenant-admins" : "租户管理员",
+            "sys-admin" : "系统管理员",
+            "tenant-admin" : "租户管理员",
+            "customer" : "客户",
+            "anonymous" : "匿名",
+            "add" : "添加用户",
+            "delete" : "删除用户",
+            "add-user-text" : "添加新用户",
+            "no-users-text" : "找不到用户",
+            "user-details" : "用户详细信息",
+            "delete-user-title" : "您确定要删除用户 '{{userEmail}}' 吗?",
+            "delete-user-text" : "小心!确认后,用户和所有相关数据将不可恢复。",
+            "delete-users-title" : "你确定你要删除 { count, select, 1 {1 user} other {# users} } 吗?",
+            "delete-users-action-title" : "删除  { count, select, 1 {1 user} other {# users} }",
+            "delete-users-text" : "小心!确认后,所有选定的用户将被删除,所有相关数据将不可恢复。",
+            "activation-email-sent-message" : "激活电子邮件已成功发送!",
+            "resend-activation" : "重新发送激活",
+            "email" : "电子邮件",
+            "email-required" : "电子邮件是必需的。",
+            "first-name" : "名字",
+            "last-name" : "姓",
+            "description" : "描述",
+            "default-dashboard" : "默认面板",
+            "always-fullscreen" : "始终全屏"
+        },
+        "value" : {
+            "type" : "值类型",
+            "string" : "字符串",
+            "string-value" : "字符串值",
+            "integer" : "数字",
+            "integer-value" : "数字值",
+            "invalid-integer-value" : "整数值无效",
+            "double" : "双精度小数",
+            "double-value" : "双精度小数值",
+            "boolean" : "布尔",
+            "boolean-value" : "布尔值",
+            "false" : "假",
+            "true" : "真"
+        },
+        "widget" : {
+            "widget-library" : "小部件库",
+            "widget-bundle" : "小部件包",
+            "select-widgets-bundle" : "选择小部件包",
+            "management" : "小部件管理",
+            "editor" : "小部件编辑器",
+            "widget-type-not-found" : "加载小部件配置时出现问题。<br> 可能关联的\n 小部件类型已删除。",
+            "widget-type-load-error" : "由于以下错误,小工具未加载:",
+            "remove" : "删除小部件",
+            "edit" : "编辑小部件",
+            "remove-widget-title" : "您确定要删除小部件 '{{widgetTitle}}' 吗?",
+            "remove-widget-text" : "确认后,窗口小部件和所有相关数据将不可恢复。",
+            "timeseries" : "时间序列",
+            "latest-values" : "最新值",
+            "rpc" : "控件小部件",
+            "static" : "静态小部件",
+            "select-widget-type" : "选择窗口小部件类型",
+            "missing-widget-title-error" : "小部件标题必须指定!",
+            "widget-saved" : "小部件已保存",
+            "unable-to-save-widget-error" : "无法保存窗口小部件! 小部件有错误!",
+            "save" : "保存小部件",
+            "saveAs" : "将小部件另存为",
+            "save-widget-type-as" : "将小部件类型另存为",
+            "save-widget-type-as-text" : "请输入新的小部件标题和/或选择目标小部件包",
+            "toggle-fullscreen" : "切换全屏",
+            "run" : "运行小部件",
+            "title" : "小部件标题",
+            "title-required" : "需要小部件标题。",
+            "type" : "小部件类型",
+            "resources" : "资源",
+            "resource-url" : "JavaScript/CSS URL",
+            "remove-resource" : "删除资源",
+            "add-resource" : "添加资源",
+            "html" : "HTML",
+            "tidy" : "整理",
+            "css" : "CSS",
+            "settings-schema" : "设置模式",
+            "datakey-settings-schema" : "数据键设置模式",
+            "javascript" : "Javascript",
+            "remove-widget-type-title" : "您确定要删除小部件类型 '{{widgetName}}'吗?",
+            "remove-widget-type-text" : "确认后,窗口小部件类型和所有相关数据将不可恢复。",
+            "remove-widget-type" : "删除小部件类型",
+            "add-widget-type" : "添加新的小部件类型",
+            "widget-type-load-failed-error" : "无法加载小部件类型!",
+            "widget-template-load-failed-error" : "无法加载小部件模板!",
+            "add" : "添加小部件",
+            "undo" : "撤消小部件更改",
+            "export" : "导出小部件"
+        },
+        "widgets-bundle" : {
+            "current" : "当前包",
+            "widgets-bundles" : "小部件包",
+            "add" : "添加小部件包",
+            "delete" : "删除小部件包",
+            "title" : "标题",
+            "title-required" : "标题是必填项。",
+            "add-widgets-bundle-text" : "添加新的小部件包",
+            "no-widgets-bundles-text" : "找不到小部件包",
+            "empty" : "小部件包是空的",
+            "details" : "详情",
+            "widgets-bundle-details" : "小部件包详细信息",
+            "delete-widgets-bundle-title" : "您确定要删除小部件包 '{{widgetsBundleTitle}}'吗?",
+            "delete-widgets-bundle-text" : "小心!确认后,小部件包和所有相关数据将不可恢复。",
+            "delete-widgets-bundles-title" : "你确定你要删除 { count, select, 1 {1 widgets bundle} other {# widgets bundles} } 吗?",
+            "delete-widgets-bundles-action-title" : "删除  { count, select, 1 {1 widgets bundle} other {# widgets bundles} }",
+            "delete-widgets-bundles-text" : "小心!确认后,所有选定的小部件包将被删除,所有相关数据将不可恢复。",
+            "no-widgets-bundles-matching" : "没有找到与 '{{widgetsBundle}}' 匹配的小部件包。",
+            "widgets-bundle-required" : "需要小部件包。",
+            "system" : "系统",
+            "import" : "导入小部件包",
+            "export" : "导出小部件包",
+            "export-failed-error" : "无法导出小部件包: {{error}}",
+            "create-new-widgets-bundle" : "创建新的小部件包",
+            "widgets-bundle-file" : "小部件包文件",
+            "invalid-widgets-bundle-file-error" : "无法导入小部件包:无效的小部件包数据结构。"
+        },
+        "widget-config" : {
+            "data" : "数据",
+            "settings" : "设置",
+            "advanced" : "高级",
+            "title" : "标题",
+            "general-settings" : "常规设置",
+            "display-title" : "显示标题",
+            "drop-shadow" : "阴影",
+            "enable-fullscreen" : "启用全屏",
+            "background-color" : "背景颜色",
+            "text-color" : "文字颜色",
+            "padding" : "填充",
+            "title-style" : "标题风格",
+            "mobile-mode-settings" : "移动模式设置",
+            "order" : "顺序",
+            "height" : "高度",
+            "units" : "特殊符号展示值",
+            "decimals" : "浮点数后的位数",
+            "timewindow" : "时间窗口",
+            "use-dashboard-timewindow" : "使用仪表板的时间窗口",
+            "display-legend" : "显示图例",
+            "datasources" : "数据源",
+            "datasource-type" : "类型",
+            "datasource-parameters" : "参数",
+            "remove-datasource" : "移除数据源",
+            "add-datasource" : "添加数据源",
+            "target-device" : "目标设备"
+        },
+        "widget-type" : {
+            "import" : "导入小部件类型",
+            "export" : "导出小部件类型",
+            "export-failed-error" : "无法导出小部件类型: {{error}}",
+            "create-new-widget-type" : "创建新的小部件类型",
+            "widget-type-file" : "小部件类型文件",
+            "invalid-widget-type-file-error" : "无法导入小部件类型:无效的小部件类型数据结构。"
+        },
+        "language" : {
+            "language" : "语言",
+            "en_US" : "英语",
+            "ko_KR" : "韩语",
+            "zh_CN" : "汉语",
+            "ru_RU" : "俄语",
+            "es_ES": "西班牙語"
+        }
+    };
+    angular.extend(locales, {
+        'zh_CN' : zh_CN
+    });
+}
\ No newline at end of file
diff --git a/ui/src/app/plugin/plugin.directive.js b/ui/src/app/plugin/plugin.directive.js
index d1fdeea..caaa7fa 100644
--- a/ui/src/app/plugin/plugin.directive.js
+++ b/ui/src/app/plugin/plugin.directive.js
@@ -22,7 +22,7 @@ import pluginFieldsetTemplate from './plugin-fieldset.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function PluginDirective($compile, $templateCache, types, utils, userService, componentDescriptorService) {
+export default function PluginDirective($compile, $templateCache, $translate, types, toast, utils, userService, componentDescriptorService) {
     var linker = function (scope, element) {
         var template = $templateCache.get(pluginFieldsetTemplate);
         element.html(template);
@@ -63,6 +63,10 @@ export default function PluginDirective($compile, $templateCache, types, utils, 
             }
         }, true);
 
+        scope.onPluginIdCopied = function() {
+            toast.showSuccess($translate.instant('plugin.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
         componentDescriptorService.getComponentDescriptorsByType(types.componentType.plugin).then(
             function success(components) {
                 scope.pluginComponents = components;
diff --git a/ui/src/app/plugin/plugin-fieldset.tpl.html b/ui/src/app/plugin/plugin-fieldset.tpl.html
index 86dfeb5..b5f8742 100644
--- a/ui/src/app/plugin/plugin-fieldset.tpl.html
+++ b/ui/src/app/plugin/plugin-fieldset.tpl.html
@@ -28,6 +28,16 @@
            ng-show="!isEdit && !isReadOnly"
            class="md-raised md-primary">{{ 'plugin.delete' | translate }}</md-button>
 
+<div layout="row">
+    <md-button ngclipboard data-clipboard-action="copy"
+               ngclipboard-success="onPluginIdCopied(e)"
+               data-clipboard-text="{{plugin.id.id}}" ng-show="!isEdit"
+               class="md-raised">
+        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+        <span translate>plugin.copyId</span>
+    </md-button>
+</div>
+
 <md-content class="md-padding" layout="column" style="overflow-x: hidden">
     <fieldset ng-disabled="loading || !isEdit || isReadOnly">
         <md-input-container class="md-block">
diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html
index 04a7a09..bd8cf74 100644
--- a/ui/src/app/plugin/plugins.tpl.html
+++ b/ui/src/app/plugin/plugins.tpl.html
@@ -31,6 +31,23 @@
                  on-export-plugin="vm.exportPlugin(event, vm.grid.detailsConfig.currentItem)"
                  on-delete-plugin="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-plugin>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.plugin}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.plugin}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+                                disable-attribute-scope-selection="true">
+            </tb-attribute-table>
+        </md-tab>
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'plugin.events' | translate }}">
             <tb-event-table flex entity-type="vm.types.entityType.plugin"
                             entity-id="vm.grid.operatingItem().id.id"
diff --git a/ui/src/app/profile/profile.controller.js b/ui/src/app/profile/profile.controller.js
index 19a16da..cea51be 100644
--- a/ui/src/app/profile/profile.controller.js
+++ b/ui/src/app/profile/profile.controller.js
@@ -27,7 +27,13 @@ export default function ProfileController(userService, $scope, $document, $mdDia
 
     vm.save = save;
     vm.changePassword = changePassword;
-    vm.languageList = {en_US: {value: "en_US", name: "language.en_US"}, ko_KR: {value : "ko_KR", name: "language.ko_KR"}};
+    vm.languageList = {
+        en_US: {value : "en_US", name: "language.en_US"}, 
+        ko_KR: {value : "ko_KR", name: "language.ko_KR"},
+        zh_CN: {value : "zh_CN", name: "language.zh_CN"},
+        ru_RU: {value : "ru_RU", name: "language.ru_RU"}ñ
+        es_ES: {value : "es_ES", name: "language.es_ES"},
+    };
 
     loadProfile();
 
diff --git a/ui/src/app/rule/rule.directive.js b/ui/src/app/rule/rule.directive.js
index bd1ab40..bfd6b4c 100644
--- a/ui/src/app/rule/rule.directive.js
+++ b/ui/src/app/rule/rule.directive.js
@@ -22,7 +22,8 @@ import ruleFieldsetTemplate from './rule-fieldset.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function RuleDirective($compile, $templateCache, $mdDialog, $document, $q, pluginService, componentDialogService, componentDescriptorService, types) {
+export default function RuleDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, pluginService,
+                                      componentDialogService, componentDescriptorService, types, toast) {
     var linker = function (scope, element) {
         var template = $templateCache.get(ruleFieldsetTemplate);
         element.html(template);
@@ -91,6 +92,10 @@ export default function RuleDirective($compile, $templateCache, $mdDialog, $docu
             }
         };
 
+        scope.onRuleIdCopied = function() {
+            toast.showSuccess($translate.instant('rule.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
         scope.$watch('rule', function(newVal, prevVal) {
                 if (newVal) {
                     if (!scope.rule.filters) {
diff --git a/ui/src/app/rule/rule-fieldset.tpl.html b/ui/src/app/rule/rule-fieldset.tpl.html
index 0b6508b..0646b3f 100644
--- a/ui/src/app/rule/rule-fieldset.tpl.html
+++ b/ui/src/app/rule/rule-fieldset.tpl.html
@@ -28,6 +28,16 @@
            ng-show="!isEdit && !isReadOnly"
            class="md-raised md-primary">{{ 'rule.delete' | translate }}</md-button>
 
+<div layout="row">
+    <md-button ngclipboard data-clipboard-action="copy"
+               ngclipboard-success="onRuleIdCopied(e)"
+               data-clipboard-text="{{rule.id.id}}" ng-show="!isEdit"
+               class="md-raised">
+        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+        <span translate>rule.copyId</span>
+    </md-button>
+</div>
+
 <md-content class="md-padding tb-rule" layout="column">
     <fieldset ng-disabled="loading || !isEdit || isReadOnly">
         <md-input-container class="md-block">
diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html
index 8aca1e3..bd7ea48 100644
--- a/ui/src/app/rule/rules.tpl.html
+++ b/ui/src/app/rule/rules.tpl.html
@@ -31,6 +31,23 @@
                                on-export-rule="vm.exportRule(event, vm.grid.detailsConfig.currentItem)"
                                on-delete-rule="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule>
         </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rule}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rule}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+                                disable-attribute-scope-selection="true">
+            </tb-attribute-table>
+        </md-tab>
         <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'rule.events' | translate }}">
             <tb-event-table flex entity-type="vm.types.entityType.rule"
                             entity-id="vm.grid.operatingItem().id.id"
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 0ac71f5..4b54b4d 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,7 +24,7 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
     .name;
 
 /*@ngInject*/
-function ItemBuffer(bufferStore, types) {
+function ItemBuffer(bufferStore, types, dashboardUtils) {
 
     const WIDGET_ITEM = "widget_item";
 
@@ -43,35 +43,26 @@ function ItemBuffer(bufferStore, types) {
         datasourceAliases: {
             datasourceIndex: {
                 aliasName: "...",
-                deviceFilter: "..."
+                entityType: "...",
+                entityFilter: "..."
             }
         }
         targetDeviceAliases: {
             targetDeviceAliasIndex: {
                 aliasName: "...",
-                deviceFilter: "..."
+                entityType: "...",
+                entityFilter: "..."
             }
         }
         ....
      }
     **/
 
-    function getDeviceFilter(alias) {
-        if (alias.deviceId) {
-            return {
-                useFilter: false,
-                deviceNameFilter: '',
-                deviceList: [alias.deviceId]
-            };
-        } else {
-            return alias.deviceFilter;
-        }
-    }
-
-    function prepareAliasInfo(deviceAlias) {
+    function prepareAliasInfo(entityAlias) {
         return {
-            aliasName: deviceAlias.alias,
-            deviceFilter: getDeviceFilter(deviceAlias)
+            aliasName: entityAlias.alias,
+            entityType: entityAlias.entityType,
+            entityFilter: entityAlias.entityFilter
         };
     }
 
@@ -86,15 +77,15 @@ function ItemBuffer(bufferStore, types) {
             originalColumns = dashboard.configuration.gridSettings.columns;
         }
         if (widget.config && dashboard.configuration
-            && dashboard.configuration.deviceAliases) {
-            var deviceAlias;
+            && dashboard.configuration.entityAliases) {
+            var entityAlias;
             if (widget.config.datasources) {
                 for (var i=0;i<widget.config.datasources.length;i++) {
                     var datasource = widget.config.datasources[i];
-                    if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
-                        deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
-                        if (deviceAlias) {
-                            aliasesInfo.datasourceAliases[i] = prepareAliasInfo(deviceAlias);
+                    if (datasource.type === types.datasourceType.entity && datasource.entityAliasId) {
+                        entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
+                        if (entityAlias) {
+                            aliasesInfo.datasourceAliases[i] = prepareAliasInfo(entityAlias);
                         }
                     }
                 }
@@ -103,9 +94,9 @@ function ItemBuffer(bufferStore, types) {
                 for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
                     var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
                     if (targetDeviceAliasId) {
-                        deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
-                        if (deviceAlias) {
-                            aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(deviceAlias);
+                        entityAlias = dashboard.configuration.entityAliases[targetDeviceAliasId];
+                        if (entityAlias) {
+                            aliasesInfo.targetDeviceAliases[i] = prepareAliasInfo(entityAlias);
                         }
                     }
                 }
@@ -151,17 +142,11 @@ function ItemBuffer(bufferStore, types) {
         } else {
             theDashboard = {};
         }
-        if (!theDashboard.configuration) {
-            theDashboard.configuration = {};
-        }
-        if (!theDashboard.configuration.deviceAliases) {
-            theDashboard.configuration.deviceAliases = {};
-        }
-        var newDeviceAliases = updateAliases(theDashboard, widget, aliasesInfo);
 
-        if (!theDashboard.configuration.widgets) {
-            theDashboard.configuration.widgets = [];
-        }
+        theDashboard = dashboardUtils.validateAndUpdateDashboard(theDashboard);
+
+        var newEntityAliases = updateAliases(theDashboard, widget, aliasesInfo);
+
         var targetColumns = 24;
         if (theDashboard.configuration.gridSettings &&
             theDashboard.configuration.gridSettings.columns) {
@@ -187,9 +172,9 @@ function ItemBuffer(bufferStore, types) {
             widget.row = row;
             widget.col = 0;
         }
-        var aliasesUpdated = !angular.equals(newDeviceAliases, theDashboard.configuration.deviceAliases);
+        var aliasesUpdated = !angular.equals(newEntityAliases, theDashboard.configuration.entityAliases);
         if (aliasesUpdated) {
-            theDashboard.configuration.deviceAliases = newDeviceAliases;
+            theDashboard.configuration.entityAliases = newEntityAliases;
             if (onAliasesUpdate) {
                 onAliasesUpdate();
             }
@@ -199,57 +184,56 @@ function ItemBuffer(bufferStore, types) {
     }
 
     function updateAliases(dashboard, widget, aliasesInfo) {
-        var deviceAliases = angular.copy(dashboard.configuration.deviceAliases);
+        var entityAliases = angular.copy(dashboard.configuration.entityAliases);
         var aliasInfo;
         var newAliasId;
         for (var datasourceIndex in aliasesInfo.datasourceAliases) {
             aliasInfo = aliasesInfo.datasourceAliases[datasourceIndex];
-            newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
-            widget.config.datasources[datasourceIndex].deviceAliasId = newAliasId;
+            newAliasId = getEntityAliasId(entityAliases, aliasInfo);
+            widget.config.datasources[datasourceIndex].entityAliasId = newAliasId;
         }
         for (var targetDeviceAliasIndex in aliasesInfo.targetDeviceAliases) {
             aliasInfo = aliasesInfo.targetDeviceAliases[targetDeviceAliasIndex];
-            newAliasId = getDeviceAliasId(deviceAliases, aliasInfo);
+            newAliasId = getEntityAliasId(entityAliases, aliasInfo);
             widget.config.targetDeviceAliasIds[targetDeviceAliasIndex] = newAliasId;
         }
-        return deviceAliases;
+        return entityAliases;
     }
 
-    function isDeviceFiltersEqual(alias1, alias2) {
-        var filter1 = getDeviceFilter(alias1);
-        var filter2 = getDeviceFilter(alias2);
-        return angular.equals(filter1, filter2);
+    function isEntityAliasEqual(alias1, alias2) {
+        return alias1.entityType === alias2.entityType &&
+            angular.equals(alias1.entityFilter, alias2.entityFilter);
     }
 
-    function getDeviceAliasId(deviceAliases, aliasInfo) {
+    function getEntityAliasId(entityAliases, aliasInfo) {
         var newAliasId;
-        for (var aliasId in deviceAliases) {
-            if (isDeviceFiltersEqual(deviceAliases[aliasId], aliasInfo)) {
+        for (var aliasId in entityAliases) {
+            if (isEntityAliasEqual(entityAliases[aliasId], aliasInfo)) {
                 newAliasId = aliasId;
                 break;
             }
         }
         if (!newAliasId) {
-            var newAliasName = createDeviceAliasName(deviceAliases, aliasInfo.aliasName);
+            var newAliasName = createEntityAliasName(entityAliases, aliasInfo.aliasName);
             newAliasId = 0;
-            for (aliasId in deviceAliases) {
+            for (aliasId in entityAliases) {
                 newAliasId = Math.max(newAliasId, aliasId);
             }
             newAliasId++;
-            deviceAliases[newAliasId] = {alias: newAliasName, deviceFilter: aliasInfo.deviceFilter};
+            entityAliases[newAliasId] = {alias: newAliasName, entityType: aliasInfo.entityType, entityFilter: aliasInfo.entityFilter};
         }
         return newAliasId;
     }
 
-    function createDeviceAliasName(deviceAliases, alias) {
+    function createEntityAliasName(entityAliases, alias) {
         var c = 0;
         var newAlias = angular.copy(alias);
         var unique = false;
         while (!unique) {
             unique = true;
-            for (var devAliasId in deviceAliases) {
-                var devAlias = deviceAliases[devAliasId];
-                if (newAlias === devAlias.alias) {
+            for (var entAliasId in entityAliases) {
+                var entAlias = entityAliases[entAliasId];
+                if (newAlias === entAlias.alias) {
                     c++;
                     newAlias = alias + c;
                     unique = false;
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index d0d1596..24db768 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -189,6 +189,12 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'supervisor_account'
                         },
                         {
+                            name: 'asset.assets',
+                            type: 'link',
+                            state: 'home.assets',
+                            icon: 'domain'
+                        },
+                        {
                             name: 'device.devices',
                             type: 'link',
                             state: 'home.devices',
@@ -234,6 +240,16 @@ function Menu(userService, $state, $rootScope) {
                             ]
                         },
                             {
+                                name: 'asset.management',
+                                places: [
+                                    {
+                                        name: 'asset.assets',
+                                        icon: 'domain',
+                                        state: 'home.assets'
+                                    }
+                                ]
+                            },
+                            {
                                 name: 'device.management',
                                 places: [
                                     {
@@ -268,6 +284,12 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'home'
                         },
                         {
+                            name: 'asset.assets',
+                            type: 'link',
+                            state: 'home.assets',
+                            icon: 'domain'
+                        },
+                        {
                             name: 'device.devices',
                             type: 'link',
                             state: 'home.devices',
@@ -282,6 +304,16 @@ function Menu(userService, $state, $rootScope) {
 
                     homeSections =
                         [{
+                            name: 'asset.view-assets',
+                            places: [
+                                {
+                                    name: 'asset.assets',
+                                    icon: 'domain',
+                                    state: 'home.assets'
+                                }
+                            ]
+                        },
+                        {
                             name: 'device.view-devices',
                             places: [
                                 {
diff --git a/ui/src/app/tenant/tenant.controller.js b/ui/src/app/tenant/tenant.controller.js
index 3ba3b4b..93fc982 100644
--- a/ui/src/app/tenant/tenant.controller.js
+++ b/ui/src/app/tenant/tenant.controller.js
@@ -21,7 +21,7 @@ import tenantCard from './tenant-card.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function TenantController(tenantService, $state, $stateParams, $translate) {
+export default function TenantController(tenantService, $state, $stateParams, $translate, types) {
 
     var tenantActionsList = [
         {
@@ -44,6 +44,8 @@ export default function TenantController(tenantService, $state, $stateParams, $t
 
     var vm = this;
 
+    vm.types = types;
+
     vm.tenantGridConfig = {
 
         refreshParamsFunc: null,
diff --git a/ui/src/app/tenant/tenant.directive.js b/ui/src/app/tenant/tenant.directive.js
index 13a7eba..7876061 100644
--- a/ui/src/app/tenant/tenant.directive.js
+++ b/ui/src/app/tenant/tenant.directive.js
@@ -20,10 +20,15 @@ import tenantFieldsetTemplate from './tenant-fieldset.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function TenantDirective($compile, $templateCache) {
+export default function TenantDirective($compile, $templateCache, $translate, toast) {
     var linker = function (scope, element) {
         var template = $templateCache.get(tenantFieldsetTemplate);
         element.html(template);
+
+        scope.onTenantIdCopied = function() {
+            toast.showSuccess($translate.instant('tenant.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
         $compile(element.contents())(scope);
     }
     return {
diff --git a/ui/src/app/tenant/tenant-fieldset.tpl.html b/ui/src/app/tenant/tenant-fieldset.tpl.html
index 3a6d9e6..76b0c3f 100644
--- a/ui/src/app/tenant/tenant-fieldset.tpl.html
+++ b/ui/src/app/tenant/tenant-fieldset.tpl.html
@@ -18,6 +18,16 @@
 <md-button ng-click="onManageUsers({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'tenant.manage-tenant-admins' | translate }}</md-button>
 <md-button ng-click="onDeleteTenant({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ 'tenant.delete' | translate }}</md-button>
 
+<div layout="row">
+	<md-button ngclipboard data-clipboard-action="copy"
+			   ngclipboard-success="onTenantIdCopied(e)"
+			   data-clipboard-text="{{tenant.id.id}}" ng-show="!isEdit"
+			   class="md-raised">
+		<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+		<span translate>tenant.copyId</span>
+	</md-button>
+</div>
+
 <md-content class="md-padding" layout="column">
 	<fieldset ng-disabled="loading || !isEdit">
 		<md-input-container class="md-block">
diff --git a/ui/src/app/tenant/tenants.tpl.html b/ui/src/app/tenant/tenants.tpl.html
index 704ec0b..ada2a3f 100644
--- a/ui/src/app/tenant/tenants.tpl.html
+++ b/ui/src/app/tenant/tenants.tpl.html
@@ -15,13 +15,43 @@
     limitations under the License.
 
 -->
+
 <tb-grid grid-configuration="vm.tenantGridConfig">
 	<details-buttons tb-help="'tenants'" help-container-id="help-container">
 		<div id="help-container"></div>
 	</details-buttons>
-	<tb-tenant tenant="vm.grid.operatingItem()"
-			   is-edit="vm.grid.detailsConfig.isDetailsEditMode"
-			   the-form="vm.grid.detailsForm"
-			   on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)"
-			   on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-tenant>
+	<md-tabs ng-class="{'tb-headless': vm.grid.detailsConfig.isDetailsEditMode}"
+			 id="tabs" md-border-bottom flex class="tb-absolute-fill">
+		<md-tab label="{{ 'tenant.details' | translate }}">
+			<tb-tenant tenant="vm.grid.operatingItem()"
+				   is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+				   the-form="vm.grid.detailsForm"
+				   on-manage-users="vm.openTenantUsers(event, vm.grid.detailsConfig.currentItem)"
+				   on-delete-tenant="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-tenant>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.attributes' | translate }}">
+			<tb-attribute-table flex
+								entity-id="vm.grid.operatingItem().id.id"
+								entity-type="{{vm.types.entityType.tenant}}"
+								entity-name="vm.grid.operatingItem().title"
+								default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+			</tb-attribute-table>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'attribute.latest-telemetry' | translate }}">
+			<tb-attribute-table flex
+								entity-id="vm.grid.operatingItem().id.id"
+								entity-type="{{vm.types.entityType.tenant}}"
+								entity-name="vm.grid.operatingItem().title"
+								default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+								disable-attribute-scope-selection="true">
+			</tb-attribute-table>
+		</md-tab>
+		<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode" label="{{ 'tenant.events' | translate }}">
+			<tb-event-table flex entity-type="vm.types.entityType.tenant"
+							entity-id="vm.grid.operatingItem().id.id"
+							tenant-id="vm.types.id.nullUid"
+							default-event-type="{{vm.types.eventType.alarm.value}}">
+			</tb-event-table>
+		</md-tab>
+	</md-tabs>
 </tb-grid>
diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js
index 4cc2192..340b7eb 100644
--- a/ui/src/app/widget/lib/flot-widget.js
+++ b/ui/src/app/widget/lib/flot-widget.js
@@ -34,39 +34,12 @@ export default class TbFlot {
         this.chartType = chartType || 'line';
         var settings = ctx.settings;
 
-        var colors = [];
-        for (var i = 0; i < ctx.data.length; i++) {
-            var series = ctx.data[i];
-            colors.push(series.dataKey.color);
-            var keySettings = series.dataKey.settings;
-
-            series.lines = {
-                fill: keySettings.fillLines === true,
-                show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
-            };
-
-            series.points = {
-                show: false,
-                radius: 8
-            };
-            if (keySettings.showPoints === true) {
-                series.points.show = true;
-                series.points.lineWidth = 5;
-                series.points.radius = 3;
-            }
-
             if (this.chartType === 'line' && settings.smoothLines && !series.points.show) {
                 series.curvedLines = {
                     apply: true
                 }
             }
 
-            var lineColor = tinycolor(series.dataKey.color);
-            lineColor.setAlpha(.75);
-
-            series.highlightColor = lineColor.toRgbString();
-
-        }
         ctx.tooltip = $('#flot-series-tooltip');
         if (ctx.tooltip.length === 0) {
             ctx.tooltip = $("<div id='flot-series-tooltip' class='flot-mouse-value'></div>");
@@ -183,7 +156,6 @@ export default class TbFlot {
         };
 
         var options = {
-            colors: colors,
             title: null,
             subtitle: null,
             shadowSize: settings.shadowSize || 4,
@@ -290,14 +262,10 @@ export default class TbFlot {
                 }
                 options.series.bars ={
                         show: true,
-                        barWidth: ctx.timeWindow.interval * 0.6,
                         lineWidth: 0,
                         fill: 0.9
                 }
             }
-
-            options.xaxis.min = ctx.timeWindow.minTime;
-            options.xaxis.max = ctx.timeWindow.maxTime;
         } else if (this.chartType === 'pie') {
             options.series = {
                 pie: {
@@ -340,55 +308,121 @@ export default class TbFlot {
 
         this.options = options;
 
+        if (this.ctx.defaultSubscription) {
+            this.init(this.ctx.$container, this.ctx.defaultSubscription);
+        }
+    }
+
+    init($element, subscription) {
+        this.subscription = subscription;
+        this.$element = $element;
+        var colors = [];
+        for (var i = 0; i < this.subscription.data.length; i++) {
+            var series = this.subscription.data[i];
+            colors.push(series.dataKey.color);
+            var keySettings = series.dataKey.settings;
+
+            series.lines = {
+                fill: keySettings.fillLines === true,
+                show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true
+            };
+
+            series.points = {
+                show: false,
+                radius: 8
+            };
+            if (keySettings.showPoints === true) {
+                series.points.show = true;
+                series.points.lineWidth = 5;
+                series.points.radius = 3;
+            }
+
+            if (this.chartType === 'line' && this.ctx.settings.smoothLines && !series.points.show) {
+                series.curvedLines = {
+                    apply: true
+                }
+            }
+
+            var lineColor = tinycolor(series.dataKey.color);
+            lineColor.setAlpha(.75);
+
+            series.highlightColor = lineColor.toRgbString();
+
+        }
+        this.options.colors = colors;
+        if (this.chartType === 'line' || this.chartType === 'bar') {
+            if (this.chartType === 'bar') {
+                this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
+            }
+            this.options.xaxis.min = this.subscription.timeWindow.minTime;
+            this.options.xaxis.max = this.subscription.timeWindow.maxTime;
+        }
+
         this.checkMouseEvents();
 
+        if (this.ctx.plot) {
+            this.ctx.plot.destroy();
+        }
         if (this.chartType === 'pie' && this.ctx.animatedPie) {
             this.ctx.pieDataAnimationDuration = 250;
-            this.ctx.pieData = angular.copy(this.ctx.data);
+            this.pieData = angular.copy(this.subscription.data);
             this.ctx.pieRenderedData = [];
             this.ctx.pieTargetData = [];
-            for (i = 0; i < this.ctx.data.length; i++) {
-                this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])
-                    ? this.ctx.data[i].data[0][1] : 0;
+            for (i = 0; i < this.subscription.data.length; i++) {
+                this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
+                    ? this.subscription.data[i].data[0][1] : 0;
             }
             this.pieDataRendered();
-            this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options);
+            this.ctx.plot = $.plot(this.$element, this.pieData, this.options);
         } else {
-            this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
+            this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options);
         }
     }
 
     update() {
-        if (!this.isMouseInteraction && this.ctx.plot) {
-            if (this.chartType === 'line' || this.chartType === 'bar') {
-                this.options.xaxis.min = this.ctx.timeWindow.minTime;
-                this.options.xaxis.max = this.ctx.timeWindow.maxTime;
-                this.ctx.plot.getOptions().xaxes[0].min = this.ctx.timeWindow.minTime;
-                this.ctx.plot.getOptions().xaxes[0].max = this.ctx.timeWindow.maxTime;
-                if (this.chartType === 'bar') {
-                    this.options.series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
-                    this.ctx.plot.getOptions().series.bars.barWidth = this.ctx.timeWindow.interval * 0.6;
-                }
-                this.ctx.plot.setData(this.ctx.data);
-                this.ctx.plot.setupGrid();
-                this.ctx.plot.draw();
-            } else if (this.chartType === 'pie') {
-                if (this.ctx.animatedPie) {
-                    this.nextPieDataAnimation(true);
-                } else {
-                    this.ctx.plot.setData(this.ctx.data);
+        if (this.updateTimeoutHandle) {
+            this.ctx.$scope.$timeout.cancel(this.updateTimeoutHandle);
+            this.updateTimeoutHandle = null;
+        }
+        if (this.subscription) {
+            if (!this.isMouseInteraction && this.ctx.plot) {
+                if (this.chartType === 'line' || this.chartType === 'bar') {
+                    this.options.xaxis.min = this.subscription.timeWindow.minTime;
+                    this.options.xaxis.max = this.subscription.timeWindow.maxTime;
+                    this.ctx.plot.getOptions().xaxes[0].min = this.subscription.timeWindow.minTime;
+                    this.ctx.plot.getOptions().xaxes[0].max = this.subscription.timeWindow.maxTime;
+                    if (this.chartType === 'bar') {
+                        this.options.series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
+                        this.ctx.plot.getOptions().series.bars.barWidth = this.subscription.timeWindow.interval * 0.6;
+                    }
+                    this.ctx.plot.setData(this.subscription.data);
+                    this.ctx.plot.setupGrid();
                     this.ctx.plot.draw();
+                } else if (this.chartType === 'pie') {
+                    if (this.ctx.animatedPie) {
+                        this.nextPieDataAnimation(true);
+                    } else {
+                        this.ctx.plot.setData(this.subscription.data);
+                        this.ctx.plot.draw();
+                    }
                 }
+            } else if (this.isMouseInteraction && this.ctx.plot){
+                var tbFlot = this;
+                this.updateTimeoutHandle = this.ctx.$scope.$timeout(function() {
+                    tbFlot.update();
+                }, 30, false);
             }
         }
     }
 
     resize() {
-        this.ctx.plot.resize();
-        if (this.chartType !== 'pie') {
-            this.ctx.plot.setupGrid();
+        if (this.ctx.plot) {
+            this.ctx.plot.resize();
+            if (this.chartType !== 'pie') {
+                this.ctx.plot.setupGrid();
+            }
+            this.ctx.plot.draw();
         }
-        this.ctx.plot.draw();
     }
 
     static get pieSettingsSchema() {
@@ -708,17 +742,19 @@ export default class TbFlot {
         var enabled = !this.ctx.isMobile &&  !this.ctx.isEdit;
         if (angular.isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled != enabled) {
             this.mouseEventsEnabled = enabled;
-            if (enabled) {
-                this.enableMouseEvents();
-            } else {
-                this.disableMouseEvents();
-            }
-            if (this.ctx.plot) {
-                this.ctx.plot.destroy();
-                if (this.chartType === 'pie' && this.ctx.animatedPie) {
-                    this.ctx.plot = $.plot(this.ctx.$container, this.ctx.pieData, this.options);
+            if (this.$element) {
+                if (enabled) {
+                    this.enableMouseEvents();
                 } else {
-                    this.ctx.plot = $.plot(this.ctx.$container, this.ctx.data, this.options);
+                    this.disableMouseEvents();
+                }
+                if (this.ctx.plot) {
+                    this.ctx.plot.destroy();
+                    if (this.chartType === 'pie' && this.ctx.animatedPie) {
+                        this.ctx.plot = $.plot(this.$element, this.pieData, this.options);
+                    } else {
+                        this.ctx.plot = $.plot(this.$element, this.subscription.data, this.options);
+                    }
                 }
             }
         }
@@ -732,8 +768,8 @@ export default class TbFlot {
     }
 
     enableMouseEvents() {
-        this.ctx.$container.css('pointer-events','');
-        this.ctx.$container.addClass('mouse-events');
+        this.$element.css('pointer-events','');
+        this.$element.addClass('mouse-events');
         this.options.selection = { mode : 'x' };
 
         var tbFlot = this;
@@ -794,33 +830,33 @@ export default class TbFlot {
                     tbFlot.ctx.plot.unhighlight();
                 }
             };
-            this.ctx.$container.bind('plothover', this.flotHoverHandler);
+            this.$element.bind('plothover', this.flotHoverHandler);
         }
 
         if (!this.flotSelectHandler) {
             this.flotSelectHandler =  function (event, ranges) {
                 tbFlot.ctx.plot.clearSelection();
-                tbFlot.ctx.timewindowFunctions.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to);
+                tbFlot.subscription.onUpdateTimewindow(ranges.xaxis.from, ranges.xaxis.to);
             };
-            this.ctx.$container.bind('plotselected', this.flotSelectHandler);
+            this.$element.bind('plotselected', this.flotSelectHandler);
         }
         if (!this.dblclickHandler) {
             this.dblclickHandler =  function () {
-                tbFlot.ctx.timewindowFunctions.onResetTimewindow();
+                tbFlot.subscription.onResetTimewindow();
             };
-            this.ctx.$container.bind('dblclick', this.dblclickHandler);
+            this.$element.bind('dblclick', this.dblclickHandler);
         }
         if (!this.mousedownHandler) {
             this.mousedownHandler =  function () {
                 tbFlot.isMouseInteraction = true;
             };
-            this.ctx.$container.bind('mousedown', this.mousedownHandler);
+            this.$element.bind('mousedown', this.mousedownHandler);
         }
         if (!this.mouseupHandler) {
             this.mouseupHandler =  function () {
                 tbFlot.isMouseInteraction = false;
             };
-            this.ctx.$container.bind('mouseup', this.mouseupHandler);
+            this.$element.bind('mouseup', this.mouseupHandler);
         }
         if (!this.mouseleaveHandler) {
             this.mouseleaveHandler =  function () {
@@ -829,38 +865,38 @@ export default class TbFlot {
                 tbFlot.ctx.plot.unhighlight();
                 tbFlot.isMouseInteraction = false;
             };
-            this.ctx.$container.bind('mouseleave', this.mouseleaveHandler);
+            this.$element.bind('mouseleave', this.mouseleaveHandler);
         }
     }
 
     disableMouseEvents() {
-        this.ctx.$container.css('pointer-events','none');
-        this.ctx.$container.removeClass('mouse-events');
+        this.$element.css('pointer-events','none');
+        this.$element.removeClass('mouse-events');
         this.options.selection = { mode : null };
 
         if (this.flotHoverHandler) {
-            this.ctx.$container.unbind('plothover', this.flotHoverHandler);
+            this.$element.unbind('plothover', this.flotHoverHandler);
             this.flotHoverHandler = null;
         }
 
         if (this.flotSelectHandler) {
-            this.ctx.$container.unbind('plotselected', this.flotSelectHandler);
+            this.$element.unbind('plotselected', this.flotSelectHandler);
             this.flotSelectHandler = null;
         }
         if (this.dblclickHandler) {
-            this.ctx.$container.unbind('dblclick', this.dblclickHandler);
+            this.$element.unbind('dblclick', this.dblclickHandler);
             this.dblclickHandler = null;
         }
         if (this.mousedownHandler) {
-            this.ctx.$container.unbind('mousedown', this.mousedownHandler);
+            this.$element.unbind('mousedown', this.mousedownHandler);
             this.mousedownHandler = null;
         }
         if (this.mouseupHandler) {
-            this.ctx.$container.unbind('mouseup', this.mouseupHandler);
+            this.$element.unbind('mouseup', this.mouseupHandler);
             this.mouseupHandler = null;
         }
         if (this.mouseleaveHandler) {
-            this.ctx.$container.unbind('mouseleave', this.mouseleaveHandler);
+            this.$element.unbind('mouseleave', this.mouseleaveHandler);
             this.mouseleaveHandler = null;
         }
     }
@@ -952,10 +988,10 @@ export default class TbFlot {
         for (var i = 0; i < this.ctx.pieTargetData.length; i++) {
             var value = this.ctx.pieTargetData[i] ? this.ctx.pieTargetData[i] : 0;
             this.ctx.pieRenderedData[i] = value;
-            if (!this.ctx.pieData[i].data[0]) {
-                this.ctx.pieData[i].data[0] = [0,0];
+            if (!this.pieData[i].data[0]) {
+                this.pieData[i].data[0] = [0,0];
             }
-            this.ctx.pieData[i].data[0][1] = value;
+            this.pieData[i].data[0][1] = value;
         }
     }
 
@@ -963,9 +999,9 @@ export default class TbFlot {
         if (start) {
             this.finishPieDataAnimation();
             this.ctx.pieAnimationStartTime = this.ctx.pieAnimationLastTime = Date.now();
-            for (var i = 0;  i < this.ctx.data.length; i++) {
-                this.ctx.pieTargetData[i] = (this.ctx.data[i].data && this.ctx.data[i].data[0])
-                    ? this.ctx.data[i].data[0][1] : 0;
+            for (var i = 0;  i < this.subscription.data.length; i++) {
+                this.ctx.pieTargetData[i] = (this.subscription.data[i].data && this.subscription.data[i].data[0])
+                    ? this.subscription.data[i].data[0][1] : 0;
             }
         }
         if (this.ctx.pieAnimationCaf) {
@@ -992,12 +1028,12 @@ export default class TbFlot {
                     var prevValue = this.ctx.pieRenderedData[i];
                     var targetValue = this.ctx.pieTargetData[i];
                     var value = prevValue + (targetValue - prevValue) * progress;
-                    if (!this.ctx.pieData[i].data[0]) {
-                        this.ctx.pieData[i].data[0] = [0,0];
+                    if (!this.pieData[i].data[0]) {
+                        this.pieData[i].data[0] = [0,0];
                     }
-                    this.ctx.pieData[i].data[0][1] = value;
+                    this.pieData[i].data[0][1] = value;
                 }
-                this.ctx.plot.setData(this.ctx.pieData);
+                this.ctx.plot.setData(this.pieData);
                 this.ctx.plot.draw();
                 this.ctx.pieAnimationLastTime = time;
             }
@@ -1007,7 +1043,7 @@ export default class TbFlot {
 
     finishPieDataAnimation() {
         this.pieDataRendered();
-        this.ctx.plot.setData(this.ctx.pieData);
+        this.ctx.plot.setData(this.pieData);
         this.ctx.plot.draw();
     }
 }
diff --git a/ui/src/app/widget/lib/google-map.js b/ui/src/app/widget/lib/google-map.js
index 9722839..0e104eb 100644
--- a/ui/src/app/widget/lib/google-map.js
+++ b/ui/src/app/widget/lib/google-map.js
@@ -182,7 +182,7 @@ export default class TbGoogleMap {
     /* eslint-enable no-undef */
 
     /* eslint-disable no-undef */
-    createMarker(location, settings) {
+    createMarker(location, settings, onClickListener) {
         var height = 34;
         var pinColor = settings.color.substr(1);
         var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + pinColor,
@@ -217,10 +217,21 @@ export default class TbGoogleMap {
             this.updateMarkerImage(marker, settings, settings.markerImage, settings.markerImageSize || 34);
         }
 
-        this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
+        if (settings.displayTooltip) {
+            this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
+        }
+
+        if (onClickListener) {
+            marker.addListener('click', onClickListener);
+        }
 
         return marker;
     }
+
+    removeMarker(marker) {
+        marker.setMap(null);
+    }
+
     /* eslint-enable no-undef */
 
     /* eslint-disable no-undef */
@@ -266,6 +277,10 @@ export default class TbGoogleMap {
     }
     /* eslint-enable no-undef */
 
+    removePolyline(polyline) {
+        polyline.setMap(null);
+    }
+
     /* eslint-disable no-undef */
     fitBounds(bounds) {
         if (this.dontFitMapBounds && this.defaultZoomLevel) {
diff --git a/ui/src/app/widget/lib/map-widget.js b/ui/src/app/widget/lib/map-widget.js
index be7b118..88ef1ca 100644
--- a/ui/src/app/widget/lib/map-widget.js
+++ b/ui/src/app/widget/lib/map-widget.js
@@ -19,11 +19,62 @@ import tinycolor from 'tinycolor2';
 import TbGoogleMap from './google-map';
 import TbOpenStreetMap from './openstreet-map';
 
+function procesTooltipPattern(tbMap, pattern, datasources, dsIndex) {
+    var match = tbMap.varsRegex.exec(pattern);
+    var replaceInfo = {};
+    replaceInfo.variables = [];
+    while (match !== null) {
+        var variableInfo = {};
+        variableInfo.dataKeyIndex = -1;
+        var variable = match[0];
+        var label = match[1];
+        var valDec = 2;
+        var splitVals = label.split(':');
+        if (splitVals.length > 1) {
+            label = splitVals[0];
+            valDec = parseFloat(splitVals[1]);
+        }
+        variableInfo.variable = variable;
+        variableInfo.valDec = valDec;
+
+        if (label.startsWith('#')) {
+            var keyIndexStr = label.substring(1);
+            var n = Math.floor(Number(keyIndexStr));
+            if (String(n) === keyIndexStr && n >= 0) {
+                variableInfo.dataKeyIndex = n;
+            }
+        }
+        if (variableInfo.dataKeyIndex === -1) {
+            var offset = 0;
+            for (var i=0;i<datasources.length;i++) {
+                var datasource = datasources[i];
+                if (angular.isUndefined(dsIndex) || dsIndex == i) {
+                    for (var k = 0; k < datasource.dataKeys.length; k++) {
+                        var dataKey = datasource.dataKeys[k];
+                        if (dataKey.label === label) {
+                            variableInfo.dataKeyIndex = offset + k;
+                            break;
+                        }
+                    }
+                }
+                offset += datasource.dataKeys.length;
+            }
+        }
+        replaceInfo.variables.push(variableInfo);
+        match = tbMap.varsRegex.exec(pattern);
+    }
+    return replaceInfo;
+}
+
+
 export default class TbMapWidget {
-    constructor(mapProvider, drawRoutes, ctx) {
+    constructor(mapProvider, drawRoutes, ctx, useDynamicLocations, $element) {
 
         var tbMap = this;
         this.ctx = ctx;
+        if (!$element) {
+            $element = ctx.$container;
+        }
 
         this.drawRoutes = drawRoutes;
         this.markers = [];
@@ -35,6 +86,9 @@ export default class TbMapWidget {
 
         var settings = ctx.settings;
 
+        this.callbacks = {};
+        this.callbacks.onLocationClick = function(){};
+
         if (settings.defaultZoomLevel) {
             if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {
                 this.defaultZoomLevel = Math.floor(settings.defaultZoomLevel);
@@ -43,52 +97,116 @@ export default class TbMapWidget {
 
         this.dontFitMapBounds = settings.fitMapBounds === false;
 
-        function procesTooltipPattern(pattern) {
-            var match = tbMap.varsRegex.exec(pattern);
-            var replaceInfo = {};
-            replaceInfo.variables = [];
-            while (match !== null) {
-                var variableInfo = {};
-                variableInfo.dataKeyIndex = -1;
-                var variable = match[0];
-                var label = match[1];
-                var valDec = 2;
-                var splitVals = label.split(':');
-                if (splitVals.length > 1) {
-                    label = splitVals[0];
-                    valDec = parseFloat(splitVals[1]);
+        if (!useDynamicLocations) {
+            this.subscription = this.ctx.defaultSubscription;
+            this.configureLocationsFromSettings();
+        }
+
+        var minZoomLevel = this.drawRoutes ? 18 : 15;
+
+        var initCallback = function() {
+              tbMap.update();
+              tbMap.resize();
+        };
+
+        if (mapProvider === 'google-map') {
+            this.map = new TbGoogleMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
+        } else if (mapProvider === 'openstreet-map') {
+            this.map = new TbOpenStreetMap($element, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel);
+        }
+
+    }
+
+    setCallbacks(callbacks) {
+        Object.assign(this.callbacks, callbacks);
+    }
+
+    clearLocations() {
+        if (this.locations) {
+            var tbMap = this;
+            this.locations.forEach(function(location) {
+                if (location.marker) {
+                    tbMap.map.removeMarker(location.marker);
                 }
-                variableInfo.variable = variable;
-                variableInfo.valDec = valDec;
-
-                if (label.startsWith('#')) {
-                    var keyIndexStr = label.substring(1);
-                    var n = Math.floor(Number(keyIndexStr));
-                    if (String(n) === keyIndexStr && n >= 0) {
-                        variableInfo.dataKeyIndex = n;
-                    }
+                if (location.polyline) {
+                    tbMap.map.removePolyline(location.polyline);
                 }
-                if (variableInfo.dataKeyIndex === -1) {
-                    var offset = 0;
-                    for (var i=0;i<ctx.datasources.length;i++) {
-                        var datasource = ctx.datasources[i];
-                        for (var k = 0; k < datasource.dataKeys.length; k++) {
-                            var dataKey = datasource.dataKeys[k];
-                            if (dataKey.label === label) {
-                                variableInfo.dataKeyIndex = offset + k;
-                                break;
-                            }
+            });
+            this.locations = null;
+            this.markers = [];
+            if (this.drawRoutes) {
+                this.polylines = [];
+            }
+        }
+    }
+
+    configureLocationsFromSubscription(subscription, subscriptionLocationSettings) {
+        this.subscription = subscription;
+        this.clearLocations();
+        this.locationsSettings = [];
+        var latKeyName = subscriptionLocationSettings.latKeyName;
+        var lngKeyName = subscriptionLocationSettings.lngKeyName;
+        var index = 0;
+        for (var i=0;i<subscription.datasources.length;i++) {
+            var datasource = subscription.datasources[i];
+            var dataKeys = datasource.dataKeys;
+            var latKeyIndex = -1;
+            var lngKeyIndex = -1;
+            var localLatKeyName = latKeyName;
+            var localLngKeyName = lngKeyName;
+            for (var k=0;k<dataKeys.length;k++) {
+                var dataKey = dataKeys[k];
+                if (dataKey.name === latKeyName) {
+                    latKeyIndex = index;
+                    localLatKeyName = localLatKeyName + index;
+                    dataKey.locationAttrName = localLatKeyName;
+                } else if (dataKey.name === lngKeyName) {
+                    lngKeyIndex = index;
+                    localLngKeyName = localLngKeyName + index;
+                    dataKey.locationAttrName = localLngKeyName;
+                }
+                if (latKeyIndex > -1 && lngKeyIndex > -1) {
+                    var locationsSettings = {
+                        latKeyName: localLatKeyName,
+                        lngKeyName: localLngKeyName,
+                        showLabel: subscriptionLocationSettings.showLabel !== false,
+                        displayTooltip: subscriptionLocationSettings.displayTooltip !== false,
+                        label: datasource.name,
+                        labelColor: subscriptionLocationSettings.labelColor || this.ctx.widgetConfig.color || '#000000',
+                        color: "#FE7569",
+                        useColorFunction: false,
+                        colorFunction: null,
+                        markerImage: null,
+                        markerImageSize: 34,
+                        useMarkerImage: false,
+                        useMarkerImageFunction: false,
+                        markerImageFunction: null,
+                        markerImages: [],
+                        tooltipPattern: subscriptionLocationSettings.tooltipPattern || "<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}"
+                    };
+
+                    locationsSettings.tooltipReplaceInfo = procesTooltipPattern(this, locationsSettings.tooltipPattern, this.subscription.datasources, i);
+
+                    locationsSettings.useColorFunction = subscriptionLocationSettings.useColorFunction === true;
+                    if (angular.isDefined(subscriptionLocationSettings.colorFunction) && subscriptionLocationSettings.colorFunction.length > 0) {
+                        try {
+                            locationsSettings.colorFunction = new Function('data, dsData, dsIndex', subscriptionLocationSettings.colorFunction);
+                        } catch (e) {
+                            locationsSettings.colorFunction = null;
                         }
-                        offset += datasource.dataKeys.length;
                     }
+
+                    this.locationsSettings.push(locationsSettings);
+                    latKeyIndex = -1;
+                    lngKeyIndex = -1;
                 }
-                replaceInfo.variables.push(variableInfo);
-                match = tbMap.varsRegex.exec(pattern);
+                index++;
             }
-            return replaceInfo;
         }
+    }
 
-        var configuredLocationsSettings = drawRoutes ? settings.routesSettings : settings.markersSettings;
+    configureLocationsFromSettings() {
+        var configuredLocationsSettings = this.drawRoutes ? this.ctx.settings.routesSettings : this.ctx.settings.markersSettings;
         if (!configuredLocationsSettings) {
             configuredLocationsSettings = [];
         }
@@ -98,8 +216,9 @@ export default class TbMapWidget {
                 latKeyName: "lat",
                 lngKeyName: "lng",
                 showLabel: true,
+                displayTooltip: true,
                 label: "",
-                labelColor: ctx.widgetConfig.color || '#000000',
+                labelColor: this.ctx.widgetConfig.color || '#000000',
                 color: "#FE7569",
                 useColorFunction: false,
                 colorFunction: null,
@@ -112,7 +231,7 @@ export default class TbMapWidget {
                 tooltipPattern: "<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}"
             };
 
-            if (drawRoutes) {
+            if (this.drawRoutes) {
                 this.locationsSettings[i].strokeWeight = 2;
                 this.locationsSettings[i].strokeOpacity = 1.0;
             }
@@ -123,7 +242,7 @@ export default class TbMapWidget {
 
                 this.locationsSettings[i].tooltipPattern = configuredLocationsSettings[i].tooltipPattern || "<b>Latitude:</b> ${"+this.locationsSettings[i].latKeyName+":7}<br/><b>Longitude:</b> ${"+this.locationsSettings[i].lngKeyName+":7}";
 
-                this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this.locationsSettings[i].tooltipPattern);
+                this.locationsSettings[i].tooltipReplaceInfo = procesTooltipPattern(this, this.locationsSettings[i].tooltipPattern, this.subscription.datasources);
 
                 this.locationsSettings[i].showLabel = configuredLocationsSettings[i].showLabel !== false;
                 this.locationsSettings[i].label = configuredLocationsSettings[i].label || this.locationsSettings[i].label;
@@ -132,7 +251,7 @@ export default class TbMapWidget {
                 this.locationsSettings[i].useColorFunction = configuredLocationsSettings[i].useColorFunction === true;
                 if (angular.isDefined(configuredLocationsSettings[i].colorFunction) && configuredLocationsSettings[i].colorFunction.length > 0) {
                     try {
-                        this.locationsSettings[i].colorFunction = new Function('data', configuredLocationsSettings[i].colorFunction);
+                        this.locationsSettings[i].colorFunction = new Function('data, dsData, dsIndex', configuredLocationsSettings[i].colorFunction);
                     } catch (e) {
                         this.locationsSettings[i].colorFunction = null;
                     }
@@ -141,7 +260,7 @@ export default class TbMapWidget {
                 this.locationsSettings[i].useMarkerImageFunction = configuredLocationsSettings[i].useMarkerImageFunction === true;
                 if (angular.isDefined(configuredLocationsSettings[i].markerImageFunction) && configuredLocationsSettings[i].markerImageFunction.length > 0) {
                     try {
-                        this.locationsSettings[i].markerImageFunction = new Function('data, images', configuredLocationsSettings[i].markerImageFunction);
+                        this.locationsSettings[i].markerImageFunction = new Function('data, images, dsData, dsIndex', configuredLocationsSettings[i].markerImageFunction);
                     } catch (e) {
                         this.locationsSettings[i].markerImageFunction = null;
                     }
@@ -157,26 +276,12 @@ export default class TbMapWidget {
                     this.locationsSettings[i].markerImageSize = configuredLocationsSettings[i].markerImageSize || 34;
                 }
 
-                if (drawRoutes) {
+                if (this.drawRoutes) {
                     this.locationsSettings[i].strokeWeight = configuredLocationsSettings[i].strokeWeight || this.locationsSettings[i].strokeWeight;
                     this.locationsSettings[i].strokeOpacity = angular.isDefined(configuredLocationsSettings[i].strokeOpacity) ? configuredLocationsSettings[i].strokeOpacity : this.locationsSettings[i].strokeOpacity;
                 }
             }
         }
-
-        var minZoomLevel = this.drawRoutes ? 18 : 15;
-
-        var initCallback = function() {
-              tbMap.update();
-              tbMap.resize();
-        };
-
-        if (mapProvider === 'google-map') {
-            this.map = new TbGoogleMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.gmApiKey, settings.gmDefaultMapType);
-        } else if (mapProvider === 'openstreet-map') {
-            this.map = new TbOpenStreetMap(ctx.$container, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel);
-        }
-
     }
 
     update() {
@@ -231,22 +336,22 @@ export default class TbMapWidget {
             return true;
         }
 
-        function calculateLocationColor(settings, dataMap) {
-            if (settings.useColorFunction && settings.colorFunction) {
+        function calculateLocationColor(location, dataMap) {
+            if (location.settings.useColorFunction && location.settings.colorFunction) {
                 var color = '#FE7569';
                 try {
-                    color = settings.colorFunction(dataMap);
+                    color = location.settings.colorFunction(dataMap.dataMap, dataMap.dsDataMap, location.dsIndex);
                 } catch (e) {
                     color = '#FE7569';
                 }
                 return tinycolor(color).toHexString();
             } else {
-                return settings.color;
+                return location.settings.color;
             }
         }
 
         function updateLocationColor(location, dataMap) {
-            var color = calculateLocationColor(location.settings, dataMap);
+            var color = calculateLocationColor(location, dataMap);
             if (!location.settings.calculatedColor || location.settings.calculatedColor !== color) {
                 if (!location.settings.useMarkerImage && !location.settings.useMarkerImageFunction) {
                     tbMap.map.updateMarkerColor(location.marker, color);
@@ -258,11 +363,11 @@ export default class TbMapWidget {
             }
         }
 
-        function calculateLocationMarkerImage(settings, dataMap) {
-            if (settings.useMarkerImageFunction && settings.markerImageFunction) {
+        function calculateLocationMarkerImage(location, dataMap) {
+            if (location.settings.useMarkerImageFunction && location.settings.markerImageFunction) {
                 var image = null;
                 try {
-                    image = settings.markerImageFunction(dataMap, settings.markerImages);
+                    image = location.settings.markerImageFunction(dataMap.dataMap, location.settings.markerImages, dataMap.dsDataMap, location.dsIndex);
                 } catch (e) {
                     image = null;
                 }
@@ -273,7 +378,7 @@ export default class TbMapWidget {
         }
 
         function updateLocationMarkerImage(location, dataMap) {
-            var image = calculateLocationMarkerImage(location.settings, dataMap);
+            var image = calculateLocationMarkerImage(location, dataMap);
             if (image != null && (!location.settings.calculatedImage || !angular.equals(location.settings.calculatedImage, image))) {
                 tbMap.map.updateMarkerImage(location.marker, location.settings, image.url, image.size);
                 location.settings.calculatedImage = image;
@@ -306,7 +411,11 @@ export default class TbMapWidget {
                         if (latLngs.length > 0) {
                             var markerLocation = latLngs[latLngs.length-1];
                             if (!location.marker) {
-                                location.marker = tbMap.map.createMarker(markerLocation, location.settings);
+                                location.marker = tbMap.map.createMarker(markerLocation, location.settings,
+                                    function() {
+                                        tbMap.callbacks.onLocationClick(location);
+                                    }
+                                );
                             } else {
                                 tbMap.map.setMarkerPosition(location.marker, markerLocation);
                             }
@@ -328,7 +437,9 @@ export default class TbMapWidget {
                         lng = lngData[lngData.length-1][1];
                         latLng = tbMap.map.createLatLng(lat, lng);
                         if (!location.marker) {
-                            location.marker = tbMap.map.createMarker(latLng, location.settings);
+                            location.marker = tbMap.map.createMarker(latLng, location.settings, function() {
+                                tbMap.callbacks.onLocationClick(location);
+                            });
                             tbMap.markers.push(location.marker);
                             locationChanged = true;
                         } else {
@@ -345,8 +456,12 @@ export default class TbMapWidget {
             return locationChanged;
         }
 
-        function toLabelValueMap(data) {
+        function toLabelValueMap(data, datasources) {
             var dataMap = {};
+            var dsDataMap = [];
+            for (var d=0;d<datasources.length;d++) {
+                dsDataMap[d] = {};
+            }
             for (var i = 0; i < data.length; i++) {
                 var dataKey = data[i].dataKey;
                 var label = dataKey.label;
@@ -356,30 +471,44 @@ export default class TbMapWidget {
                     val = keyData[keyData.length-1][1];
                 }
                 dataMap[label] = val;
+                var dsIndex = datasources.indexOf(data[i].datasource);
+                dsDataMap[dsIndex][label] = val;
             }
-            return dataMap;
+            return {
+                dataMap: dataMap,
+                dsDataMap: dsDataMap
+            };
         }
 
-        function loadLocations(data) {
+        function loadLocations(data, datasources) {
             var bounds = tbMap.map.createBounds();
             tbMap.locations = [];
-            var dataMap = toLabelValueMap(data);
+            var dataMap = toLabelValueMap(data, datasources);
             for (var l=0; l < tbMap.locationsSettings.length; l++) {
                 var locationSettings = tbMap.locationsSettings[l];
                 var latIndex = -1;
                 var lngIndex = -1;
                 for (var i = 0; i < data.length; i++) {
                     var dataKey = data[i].dataKey;
-                    if (dataKey.label === locationSettings.latKeyName) {
+                    var nameToCheck;
+                    if (dataKey.locationAttrName) {
+                        nameToCheck = dataKey.locationAttrName;
+                    } else {
+                        nameToCheck = dataKey.label;
+                    }
+                    if (nameToCheck === locationSettings.latKeyName) {
                         latIndex = i;
-                    } else if (dataKey.label === locationSettings.lngKeyName) {
+                    } else if (nameToCheck === locationSettings.lngKeyName) {
                         lngIndex = i;
                     }
                 }
                 if (latIndex > -1 && lngIndex > -1) {
+                    var ds = data[latIndex].datasource;
+                    var dsIndex = datasources.indexOf(ds);
                     var location = {
                         latIndex: latIndex,
                         lngIndex: lngIndex,
+                        dsIndex: dsIndex,
                         settings: locationSettings
                     };
                     tbMap.locations.push(location);
@@ -394,10 +523,10 @@ export default class TbMapWidget {
             tbMap.map.fitBounds(bounds);
         }
 
-        function updateLocations(data) {
+        function updateLocations(data, datasources) {
             var locationsChanged = false;
             var bounds = tbMap.map.createBounds();
-            var dataMap = toLabelValueMap(data);
+            var dataMap = toLabelValueMap(data, datasources);
             for (var p = 0; p < tbMap.locations.length; p++) {
                 var location = tbMap.locations[p];
                 locationsChanged |= updateLocation(location, data, dataMap);
@@ -412,36 +541,36 @@ export default class TbMapWidget {
             }
         }
 
-        if (this.map && this.map.inited()) {
-            if (this.ctx.data) {
+        if (this.map && this.map.inited() && this.subscription) {
+            if (this.subscription.data) {
                 if (!this.locations) {
-                    loadLocations(this.ctx.data);
+                    loadLocations(this.subscription.data, this.subscription.datasources);
                 } else {
-                    updateLocations(this.ctx.data);
+                    updateLocations(this.subscription.data, this.subscription.datasources);
                 }
-            }
-            var tooltips = this.map.getTooltips();
-            for (var t=0; t < tooltips.length; t++) {
-                var tooltip = tooltips[t];
-                var text = tooltip.pattern;
-                var replaceInfo = tooltip.replaceInfo;
-                for (var v = 0; v < replaceInfo.variables.length; v++) {
-                    var variableInfo = replaceInfo.variables[v];
-                    var txtVal = '';
-                    if (variableInfo.dataKeyIndex > -1) {
-                        var varData = this.ctx.data[variableInfo.dataKeyIndex].data;
-                        if (varData.length > 0) {
-                            var val = varData[varData.length - 1][1];
-                            if (isNumber(val)) {
-                                txtVal = padValue(val, variableInfo.valDec, 0);
-                            } else {
-                                txtVal = val;
+                var tooltips = this.map.getTooltips();
+                for (var t=0; t < tooltips.length; t++) {
+                    var tooltip = tooltips[t];
+                    var text = tooltip.pattern;
+                    var replaceInfo = tooltip.replaceInfo;
+                    for (var v = 0; v < replaceInfo.variables.length; v++) {
+                        var variableInfo = replaceInfo.variables[v];
+                        var txtVal = '';
+                        if (variableInfo.dataKeyIndex > -1 && this.subscription.data[variableInfo.dataKeyIndex]) {
+                            var varData = this.subscription.data[variableInfo.dataKeyIndex].data;
+                            if (varData.length > 0) {
+                                var val = varData[varData.length - 1][1];
+                                if (isNumber(val)) {
+                                    txtVal = padValue(val, variableInfo.valDec, 0);
+                                } else {
+                                    txtVal = val;
+                                }
                             }
                         }
+                        text = text.split(variableInfo.variable).join(txtVal);
                     }
-                    text = text.split(variableInfo.variable).join(txtVal);
+                    tooltip.popup.setContent(text);
                 }
-                tooltip.popup.setContent(text);
             }
         }
     }
@@ -449,7 +578,7 @@ export default class TbMapWidget {
     resize() {
         if (this.map && this.map.inited()) {
             this.map.invalidateSize();
-            if (this.locations && this.locations.size > 0) {
+            if (this.locations && this.locations.length > 0) {
                 var bounds = this.map.createBounds();
                 for (var m = 0; m < this.markers.length; m++) {
                     this.map.extendBoundsWithMarker(bounds, this.markers[m]);
diff --git a/ui/src/app/widget/lib/openstreet-map.js b/ui/src/app/widget/lib/openstreet-map.js
index aacb505..65f7c7f 100644
--- a/ui/src/app/widget/lib/openstreet-map.js
+++ b/ui/src/app/widget/lib/openstreet-map.js
@@ -85,7 +85,7 @@ export default class TbOpenStreetMap {
         testImage.src = image;
     }
 
-    createMarker(location, settings) {
+    createMarker(location, settings, onClickListener) {
         var height = 34;
         var pinColor = settings.color.substr(1);
         var icon = L.icon({
@@ -109,11 +109,21 @@ export default class TbOpenStreetMap {
             this.updateMarkerImage(marker, settings, settings.markerImage, settings.markerImageSize || 34);
         }
 
-        this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
+        if (settings.displayTooltip) {
+            this.createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);
+        }
+
+        if (onClickListener) {
+            marker.on('click', onClickListener);
+        }
 
         return marker;
     }
 
+    removeMarker(marker) {
+        this.map.removeLayer(marker);
+    }
+
     createTooltip(marker, pattern, replaceInfo) {
         var popup = L.popup();
         popup.setContent('');
@@ -145,6 +155,10 @@ export default class TbOpenStreetMap {
         return polyline;
     }
 
+    removePolyline(polyline) {
+        this.map.removeLayer(polyline);
+    }
+
     fitBounds(bounds) {
         if (bounds.isValid()) {
             if (this.dontFitMapBounds && this.defaultZoomLevel) {
diff --git a/ui/src/app/widget/lib/timeseries-table-widget.js b/ui/src/app/widget/lib/timeseries-table-widget.js
new file mode 100644
index 0000000..c55c519
--- /dev/null
+++ b/ui/src/app/widget/lib/timeseries-table-widget.js
@@ -0,0 +1,325 @@
+/*
+ * 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 './timeseries-table-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import timeseriesTableWidgetTemplate from './timeseries-table-widget.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import tinycolor from 'tinycolor2';
+import cssjs from '../../../vendor/css.js/css';
+
+export default angular.module('thingsboard.widgets.timeseriesTableWidget', [])
+    .directive('tbTimeseriesTableWidget', TimeseriesTableWidget)
+    .name;
+
+/*@ngInject*/
+function TimeseriesTableWidget() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            tableId: '=',
+            config: '=',
+            datasources: '=',
+            data: '='
+        },
+        controller: TimeseriesTableWidgetController,
+        controllerAs: 'vm',
+        templateUrl: timeseriesTableWidgetTemplate
+    };
+}
+
+/*@ngInject*/
+function TimeseriesTableWidgetController($element, $scope, $filter) {
+    var vm = this;
+
+    vm.sources = [];
+    vm.sourceIndex = 0;
+
+    $scope.$watch('vm.config', function() {
+       if (vm.config) {
+           vm.settings = vm.config.settings;
+           vm.widgetConfig = vm.config.widgetConfig;
+           initialize();
+       }
+    });
+
+    function initialize() {
+        vm.showTimestamp = vm.settings.showTimestamp !== false;
+        var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
+        var defaultColor = tinycolor(origColor);
+        var mdDark = defaultColor.setAlpha(0.87).toRgbString();
+        var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
+        var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
+        //var mdDarkIcon = mdDarkSecondary;
+        var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
+
+        var cssString = 'table.md-table th.md-column {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            'table.md-table th.md-column md-icon.md-sort-icon {\n'+
+            'color: ' + mdDarkDisabled + ';\n'+
+            '}\n'+
+            'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
+            'color: ' + mdDark + ';\n'+
+            '}\n'+
+            'table.md-table td.md-cell {\n'+
+            'color: ' + mdDark + ';\n'+
+            'border-top: 1px '+mdDarkDivider+' solid;\n'+
+            '}\n'+
+            'table.md-table td.md-cell.md-placeholder {\n'+
+            'color: ' + mdDarkDisabled + ';\n'+
+            '}\n'+
+            'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            '.md-table-pagination {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            'border-top: 1px '+mdDarkDivider+' solid;\n'+
+            '}\n'+
+            '.md-table-pagination .buttons md-icon {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}\n'+
+            '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
+            'color: ' + mdDarkSecondary + ';\n'+
+            '}';
+
+        var cssParser = new cssjs();
+        cssParser.testMode = false;
+        var namespace = 'ts-table-' + hashCode(cssString);
+        cssParser.cssPreviewNamespace = namespace;
+        cssParser.createStyleElement(namespace, cssString);
+        $element.addClass(namespace);
+
+        function hashCode(str) {
+            var hash = 0;
+            var i, char;
+            if (str.length === 0) return hash;
+            for (i = 0; i < str.length; i++) {
+                char = str.charCodeAt(i);
+                hash = ((hash << 5) - hash) + char;
+                hash = hash & hash;
+            }
+            return hash;
+        }
+    }
+
+    $scope.$watch('vm.datasources', function() {
+        updateDatasources();
+    });
+
+    $scope.$on('timeseries-table-data-updated', function(event, tableId) {
+        if (vm.tableId == tableId) {
+            dataUpdated();
+        }
+    });
+
+    function dataUpdated() {
+        for (var s=0; s < vm.sources.length; s++) {
+            var source = vm.sources[s];
+            source.rawData = vm.data.slice(source.keyStartIndex, source.keyEndIndex);
+        }
+        updateSourceData(vm.sources[vm.sourceIndex]);
+        $scope.$digest();
+    }
+
+    vm.onPaginate = function(source) {
+        updatePage(source);
+    }
+
+    vm.onReorder = function(source) {
+        reorder(source);
+        updatePage(source);
+    }
+
+    vm.cellStyle = function(source, index, value) {
+        var style = {};
+        if (index > 0) {
+            var styleInfo = source.ts.stylesInfo[index-1];
+            if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
+                try {
+                    style = styleInfo.cellStyleFunction(value);
+                } catch (e) {
+                    style = {};
+                }
+            }
+        }
+        return style;
+    }
+
+    vm.cellContent = function(source, index, row, value) {
+        if (index === 0) {
+            return $filter('date')(value, 'yyyy-MM-dd HH:mm:ss');
+        } else {
+            var strContent = '';
+            if (angular.isDefined(value)) {
+                strContent = ''+value;
+            }
+            var content = strContent;
+            var contentInfo = source.ts.contentsInfo[index-1];
+            if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
+                try {
+                    var rowData = source.ts.rowDataTemplate;
+                    rowData['Timestamp'] = row[0];
+                    for (var h=0; h < source.ts.header.length; h++) {
+                        var headerInfo = source.ts.header[h];
+                        rowData[headerInfo.dataKey.name] = row[headerInfo.index];
+                    }
+                    content = contentInfo.cellContentFunction(value, rowData, $filter);
+                } catch (e) {
+                    content = strContent;
+                }
+            }
+            return content;
+        }
+    }
+
+    $scope.$watch('vm.sourceIndex', function(newIndex, oldIndex) {
+        if (newIndex != oldIndex) {
+            updateSourceData(vm.sources[vm.sourceIndex]);
+        }
+    });
+
+    function updateDatasources() {
+        vm.sources = [];
+        vm.sourceIndex = 0;
+        var keyOffset = 0;
+        if (vm.datasources) {
+            for (var ds = 0; ds < vm.datasources.length; ds++) {
+                var source = {};
+                var datasource = vm.datasources[ds];
+                source.keyStartIndex = keyOffset;
+                keyOffset += datasource.dataKeys.length;
+                source.keyEndIndex = keyOffset;
+                source.datasource = datasource;
+                source.data = [];
+                source.rawData = [];
+                source.query = {
+                    limit: 5,
+                    page: 1,
+                    order: '-0'
+                }
+                source.ts = {
+                    header: [],
+                    count: 0,
+                    data: [],
+                    stylesInfo: [],
+                    contentsInfo: [],
+                    rowDataTemplate: {}
+                }
+                source.ts.rowDataTemplate['Timestamp'] = null;
+                for (var a = 0; a < datasource.dataKeys.length; a++ ) {
+                    var dataKey = datasource.dataKeys[a];
+                    var keySettings = dataKey.settings;
+                    source.ts.header.push({
+                        index: a+1,
+                        dataKey: dataKey
+                    });
+                    source.ts.rowDataTemplate[dataKey.label] = null;
+
+                    var cellStyleFunction = null;
+                    var useCellStyleFunction = false;
+
+                    if (keySettings.useCellStyleFunction === true) {
+                        if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
+                            try {
+                                cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
+                                useCellStyleFunction = true;
+                            } catch (e) {
+                                cellStyleFunction = null;
+                                useCellStyleFunction = false;
+                            }
+                        }
+                    }
+
+                    source.ts.stylesInfo.push({
+                        useCellStyleFunction: useCellStyleFunction,
+                        cellStyleFunction: cellStyleFunction
+                    });
+
+                    var cellContentFunction = null;
+                    var useCellContentFunction = false;
+
+                    if (keySettings.useCellContentFunction === true) {
+                        if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
+                            try {
+                                cellContentFunction = new Function('value, rowData, filter', keySettings.cellContentFunction);
+                                useCellContentFunction = true;
+                            } catch (e) {
+                                cellContentFunction = null;
+                                useCellContentFunction = false;
+                            }
+                        }
+                    }
+
+                    source.ts.contentsInfo.push({
+                        useCellContentFunction: useCellContentFunction,
+                        cellContentFunction: cellContentFunction
+                    });
+
+                }
+                vm.sources.push(source);
+            }
+        }
+    }
+
+    function updatePage(source) {
+        var startIndex = source.query.limit * (source.query.page - 1);
+        source.ts.data = source.data.slice(startIndex, startIndex + source.query.limit);
+    }
+
+    function reorder(source) {
+        source.data = $filter('orderBy')(source.data, source.query.order);
+    }
+
+    function convertData(data) {
+        var rowsMap = {};
+        for (var d = 0; d < data.length; d++) {
+            var columnData = data[d].data;
+            for (var i = 0; i < columnData.length; i++) {
+                var cellData = columnData[i];
+                var timestamp = cellData[0];
+                var row = rowsMap[timestamp];
+                if (!row) {
+                    row = [];
+                    row[0] = timestamp;
+                    for (var c = 0; c < data.length; c++) {
+                        row[c+1] = undefined;
+                    }
+                    rowsMap[timestamp] = row;
+                }
+                row[d+1] = cellData[1];
+            }
+        }
+        var rows = [];
+        for (var t in rowsMap) {
+            rows.push(rowsMap[t]);
+        }
+        return rows;
+    }
+
+    function updateSourceData(source) {
+        source.data = convertData(source.rawData);
+        source.ts.count = source.data.length;
+        reorder(source);
+        updatePage(source);
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/timeseries-table-widget.scss b/ui/src/app/widget/lib/timeseries-table-widget.scss
new file mode 100644
index 0000000..99a0653
--- /dev/null
+++ b/ui/src/app/widget/lib/timeseries-table-widget.scss
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+tb-timeseries-table-widget {
+    table.md-table thead.md-head>tr.md-row {
+        height: 40px;
+    }
+
+    table.md-table tbody.md-body>tr.md-row, table.md-table tfoot.md-foot>tr.md-row {
+        height: 38px;
+    }
+
+    .md-table-pagination>* {
+        height: 46px;
+    }
+}
diff --git a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html
new file mode 100644
index 0000000..2b6d72c
--- /dev/null
+++ b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html
@@ -0,0 +1,43 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+
+<md-tabs md-selected="vm.sourceIndex" ng-class="{'tb-headless': vm.sources.length === 1}"
+         id="tabs" md-border-bottom flex class="tb-absolute-fill">
+    <md-tab ng-repeat="source in vm.sources" label="{{ source.datasource.name }}">
+        <md-table-container>
+            <table md-table>
+                <thead md-head md-order="source.query.order" md-on-reorder="vm.onReorder(source)">
+                <tr md-row>
+                    <th ng-show="vm.showTimestamp" md-column md-order-by="0"><span>Timestamp</span></th>
+                    <th md-column md-order-by="{{ h.index }}" ng-repeat="h in source.ts.header"><span>{{ h.dataKey.label }}</span></th>
+                </tr>
+                </thead>
+                <tbody md-body>
+                <tr md-row ng-repeat="row in source.ts.data">
+                    <td ng-show="$index > 0 || ($index === 0 && vm.showTimestamp)" md-cell ng-repeat="d in row track by $index" ng-style="vm.cellStyle(source, $index, d)" ng-bind-html="vm.cellContent(source, $index, row, d)">
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </md-table-container>
+        <md-table-pagination md-limit="source.query.limit" md-limit-options="[5, 10, 15]"
+                             md-page="source.query.page" md-total="{{source.ts.count}}"
+                             md-on-paginate="vm.onPaginate(source)" md-page-select>
+        </md-table-pagination>
+    </md-tab>
+</md-tabs>
\ No newline at end of file
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 9f89aba..ab9b0d7 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -236,6 +236,32 @@ div {
   }
 }
 
+pre.tb-highlight {
+  background-color: #f7f7f7;
+  display: block;
+  margin: 20px 0;
+  padding: 15px;
+  overflow-x: auto;
+  code {
+    padding: 0;
+    color: #303030;
+    font-family: monospace;
+    display: inline-block;
+    box-sizing: border-box;
+    vertical-align: bottom;
+    font-size: 16px;
+    font-weight: bold;
+  }
+}
+
+.tb-notice {
+  background-color: #f7f7f7;
+  padding: 15px;
+  border: 1px solid #ccc;
+  font-size: 16px;
+}
+
+
 /***********************
  * Flow
  ***********************/
diff --git a/ui/webpack.config.dev.js b/ui/webpack.config.dev.js
index c669ec5..fa019d3 100644
--- a/ui/webpack.config.dev.js
+++ b/ui/webpack.config.dev.js
@@ -60,6 +60,7 @@ module.exports = {
             allChunks: true,
         }),
         new webpack.DefinePlugin({
+            THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version),
             '__DEVTOOLS__': false,
             'process.env': {
                 NODE_ENV: JSON.stringify('development'),
diff --git a/ui/webpack.config.prod.js b/ui/webpack.config.prod.js
index 09374fb..a746488 100644
--- a/ui/webpack.config.prod.js
+++ b/ui/webpack.config.prod.js
@@ -58,6 +58,7 @@ module.exports = {
             allChunks: true,
         }),
         new webpack.DefinePlugin({
+            THINGSBOARD_VERSION: JSON.stringify(require('./package.json').version),
             '__DEVTOOLS__': false,
             'process.env': {
                 NODE_ENV: JSON.stringify('production'),