thingsboard-aplcache

Changes

.travis.yml 9(+9 -0)

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

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

pom.xml 2(+1 -1)

README.md 4(+3 -1)

tools/pom.xml 41(+39 -2)

ui/.eslintrc 3(+3 -0)

ui/package.json 20(+11 -9)

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

Details

.travis.yml 9(+9 -0)

diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a7332e8
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+before_install:
+  - sudo rm /etc/mavenrc
+  - export M2_HOME=/usr/local/maven
+  - export MAVEN_OPTS="-Dmaven.repo.local=$HOME/.m2/repository -Xms1024m -Xmx3072m"
+jdk:
+ - oraclejdk8
+language: java
+sudo: required
+script: mvn clean verify
diff --git a/application/build.gradle b/application/build.gradle
index 8074ea1..ce8b10f 100644
--- a/application/build.gradle
+++ b/application/build.gradle
@@ -49,7 +49,7 @@ ospackage {
     from(mainJar) {
         // Strip the version from the jar filename
         rename { String fileName ->
-            fileName.replace("-${project.version}", "")
+            "${pkgName}.jar"
         }
         fileMode 0500
         into "bin"
diff --git a/application/pom.xml b/application/pom.xml
index 299a1c1..d0c7035 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
@@ -379,6 +379,7 @@
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
                 <configuration>
+                    <classifier>boot</classifier>
                     <layout>ZIP</layout>
                     <executable>true</executable>
                     <excludeDevtools>true</excludeDevtools>
@@ -408,7 +409,7 @@
                     <args>
                         <arg>-PprojectBuildDir=${project.build.directory}</arg>
                         <arg>-PprojectVersion=${project.version}</arg>
-                        <arg>-PmainJar=${project.build.directory}/${project.build.finalName}.${project.packaging}</arg>
+                        <arg>-PmainJar=${project.build.directory}/${project.build.finalName}-boot.${project.packaging}</arg>
                         <arg>-PpkgName=${pkg.name}</arg>
                         <arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
                         <arg>-PpkgLogFolder=${pkg.logFolder}</arg>
diff --git a/application/src/main/conf/logback.xml b/application/src/main/conf/logback.xml
index 4187356..5d53400 100644
--- a/application/src/main/conf/logback.xml
+++ b/application/src/main/conf/logback.xml
@@ -24,7 +24,7 @@
         <file>${pkg.logFolder}/${pkg.name}.log</file>
         <rollingPolicy
                 class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
-            <fileNamePattern>${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <fileNamePattern>${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
             <maxFileSize>100MB</maxFileSize>
             <maxHistory>30</maxHistory>
             <totalSizeCap>3GB</totalSizeCap>
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index c370616..8f05422 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -121,7 +121,7 @@ public class AppActor extends ContextAwareActor {
 
     private void broadcast(Object msg) {
         pluginManager.broadcast(msg);
-        tenantActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+        tenantActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
     private void onToRuleMsg(ToRuleActorMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index 8b669e9..bf18df6 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
 import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.*;
 
@@ -58,6 +59,8 @@ public class DeviceActor extends ContextAwareActor {
                 processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
             } else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
                 processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
+            } else if (msg instanceof DeviceCredentialsUpdateNotificationMsg){
+                processor.processCredentialsUpdate(context(), (DeviceCredentialsUpdateNotificationMsg) msg);
             }
         } else if (msg instanceof TimeoutMsg) {
             processor.processTimeout(context(), (TimeoutMsg) msg);
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 3949691..3aef0c8 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
@@ -32,13 +32,7 @@ import org.thingsboard.server.common.data.kv.AttributeKey;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
-import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
-import org.thingsboard.server.common.msg.core.BasicToDeviceSessionActorMsg;
-import org.thingsboard.server.common.msg.core.SessionCloseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
-import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.*;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
 import org.thingsboard.server.common.msg.session.FromDeviceMsg;
@@ -47,6 +41,7 @@ import org.thingsboard.server.common.msg.session.SessionType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
 import org.thingsboard.server.extensions.api.device.DeviceAttributes;
 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
 import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
 import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
@@ -74,6 +69,7 @@ import java.util.stream.Collectors;
 public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
 
     private final DeviceId deviceId;
+    private final Map<SessionId, SessionInfo> sessions;
     private final Map<SessionId, SessionInfo> attributeSubscriptions;
     private final Map<SessionId, SessionInfo> rpcSubscriptions;
 
@@ -85,6 +81,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
     public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
         super(systemContext, logger);
         this.deviceId = deviceId;
+        this.sessions = new HashMap<>();
         this.attributeSubscriptions = new HashMap<>();
         this.rpcSubscriptions = new HashMap<>();
         this.rpcPendingMap = new HashMap<>();
@@ -281,7 +278,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         if (!msg.isAdded()) {
             logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
             Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
-                .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
+                    .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
             attributeSubscriptions.entrySet().removeIf(filter);
             rpcSubscriptions.entrySet().removeIf(filter);
         }
@@ -342,8 +339,12 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
     private void processSessionStateMsgs(ToDeviceActorMsg msg) {
         SessionId sessionId = msg.getSessionId();
         FromDeviceMsg inMsg = msg.getPayload();
-        if (inMsg instanceof SessionCloseMsg) {
+        if (inMsg instanceof SessionOpenMsg) {
+            logger.debug("[{}] Processing new session [{}]", deviceId, sessionId);
+            sessions.put(sessionId, new SessionInfo(SessionType.ASYNC, msg.getServerAddress()));
+        } else if (inMsg instanceof SessionCloseMsg) {
             logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
+            sessions.remove(sessionId);
             attributeSubscriptions.remove(sessionId);
             rpcSubscriptions.remove(sessionId);
         }
@@ -363,4 +364,11 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
     }
 
+    public void processCredentialsUpdate(ActorContext context, DeviceCredentialsUpdateNotificationMsg msg) {
+        sessions.forEach((k, v) -> {
+            sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(new SessionCloseNotification(), k), v.getServer());
+        });
+        attributeSubscriptions.clear();
+        rpcSubscriptions.clear();
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
index 72ae4bb..c1d2678 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
@@ -181,7 +181,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
             logger.info("[{}] Plugin requires restart due to clazz change from {} to {}.",
                     entityId, oldPluginMd.getClazz(), pluginMd.getClazz());
             requiresRestart = true;
-        } else if (oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
+        } else if (!oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
             logger.info("[{}] Plugin requires restart due to configuration change from {} to {}.",
                     entityId, oldPluginMd.getConfiguration(), pluginMd.getConfiguration());
             requiresRestart = true;
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
index 82011c0..1f64a92 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
@@ -234,18 +234,18 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
         logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd);
         try {
             fetchPluginInfo();
-            if (!Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
+            if (filters == null || !Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
                 logger.info("[{}] Rule filters require restart due to json change from {} to {}.",
                         entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters()));
                 stopFilters();
                 initFilters();
             }
-            if (!Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
+            if (processor == null || !Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
                 logger.info("[{}] Rule processor require restart due to configuration change.", entityId);
                 stopProcessor();
                 initProcessor();
             }
-            if (!Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
+            if (action == null || !Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
                 logger.info("[{}] Rule action require restart due to configuration change.", entityId);
                 stopAction();
                 initAction();
@@ -272,13 +272,15 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
         if (action != null) {
             if (filters != null) {
                 filters.forEach(f -> f.resume());
+            } else {
+                initFilters();
             }
             if (processor != null) {
                 processor.resume();
+            } else {
+                initProcessor();
             }
-            if (action != null) {
-                action.resume();
-            }
+            action.resume();
             logger.info("[{}] Rule resumed.", entityId);
         } else {
             start();
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
index 8112ac4..5a8b20a 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.actors.rule;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -26,7 +25,7 @@ public class SimpleRuleActorChain implements RuleActorChain {
 
     public SimpleRuleActorChain(Set<RuleActorMetaData> ruleSet) {
         rules = new ArrayList<>(ruleSet);
-        Collections.sort(rules, RuleActorMetaData.RULE_ACTOR_MD_COMPARATOR);
+        rules.sort(RuleActorMetaData.RULE_ACTOR_MD_COMPARATOR);
     }
 
     public int size() {
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index 1c64f54..3db7210 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.actors.service;
 
+import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -28,4 +29,6 @@ public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor
     void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
 
     void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+
+    void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
 }
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 db6526d..bbf1300 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
@@ -32,16 +32,19 @@ import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
 import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
 import org.thingsboard.server.actors.session.SessionManagerActor;
 import org.thingsboard.server.actors.stats.StatsActor;
+import org.thingsboard.server.common.data.id.DeviceId;
 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.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
 import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
 import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
 import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
@@ -56,6 +59,7 @@ import scala.concurrent.duration.Duration;
 
 import javax.annotation.PostConstruct;
 import javax.annotation.PreDestroy;
+import java.util.Optional;
 
 @Service
 @Slf4j
@@ -221,6 +225,17 @@ public class DefaultActorService implements ActorService {
         broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
     }
 
+    @Override
+    public void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId) {
+        DeviceCredentialsUpdateNotificationMsg msg = new DeviceCredentialsUpdateNotificationMsg(tenantId, deviceId);
+        Optional<ServerAddress> address = actorContext.getRoutingService().resolve(deviceId);
+        if (address.isPresent()) {
+            rpcService.tell(address.get(), msg);
+        } else {
+            onMsg(msg);
+        }
+    }
+
     public void broadcast(ToAllNodesMsg msg) {
         rpcService.broadcast(msg);
         appActor.tell(msg, ActorRef.noSender());
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 eb812df..916e678 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
@@ -20,15 +20,14 @@ import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.SessionId;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
-import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
-import org.thingsboard.server.common.msg.core.ResponseMsg;
-import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
+import org.thingsboard.server.common.msg.core.*;
 import org.thingsboard.server.common.msg.core.SessionCloseMsg;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.common.msg.session.*;
 
 import akka.actor.ActorContext;
 import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.msg.session.ctrl.*;
 import org.thingsboard.server.common.msg.session.ex.SessionException;
 
 import java.util.HashMap;
@@ -37,7 +36,8 @@ import java.util.Optional;
 
 class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
 
-    Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
+    private boolean firstMsg = true;
+    private Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
     private Optional<ServerAddress> currentTargetServer;
     private boolean subscribedToAttributeUpdates;
     private boolean subscribedToRpcCommands;
@@ -49,6 +49,10 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     @Override
     protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
         updateSessionCtx(msg, SessionType.ASYNC);
+        if (firstMsg) {
+            toDeviceMsg(new SessionOpenMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
+            firstMsg = false;
+        }
         ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
         FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
         switch (fromDeviceMsg.getMsgType()) {
@@ -80,17 +84,21 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     @Override
     public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
         try {
-            switch (msg.getMsgType()) {
-                case STATUS_CODE_RESPONSE:
-                case GET_ATTRIBUTES_RESPONSE:
-                    ResponseMsg responseMsg = (ResponseMsg) msg;
-                    if (responseMsg.getRequestId() >= 0) {
-                        logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
-                        pendingMap.remove(responseMsg.getRequestId());
-                    }
-                    break;
+            if (msg.getMsgType() != MsgType.SESSION_CLOSE) {
+                switch (msg.getMsgType()) {
+                    case STATUS_CODE_RESPONSE:
+                    case GET_ATTRIBUTES_RESPONSE:
+                        ResponseMsg responseMsg = (ResponseMsg) msg;
+                        if (responseMsg.getRequestId() >= 0) {
+                            logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
+                            pendingMap.remove(responseMsg.getRequestId());
+                        }
+                        break;
+                }
+                sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
+            } else {
+                sessionCtx.onMsg(org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg.onCredentialsRevoked(sessionCtx.getSessionId()));
             }
-            sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
         } catch (SessionException e) {
             logger.warning("Failed to push session response msg", e);
         }
@@ -102,7 +110,7 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     }
 
     protected void cleanupSession(ActorContext ctx) {
-        toDeviceMsg(new SessionCloseMsg()).ifPresent(msg -> forwardToAppActor(ctx, msg));
+        toDeviceMsg(new SessionCloseMsg()).ifPresent(m -> forwardToAppActor(ctx, m));
     }
 
     @Override
@@ -110,8 +118,9 @@ class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
         if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
             Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
             if (!newTargetServer.equals(currentTargetServer)) {
+                firstMsg = true;
                 currentTargetServer = newTargetServer;
-                pendingMap.values().stream().forEach(v -> {
+                pendingMap.values().forEach(v -> {
                     forwardToAppActor(context, v, currentTargetServer);
                     if (currentTargetServer.isPresent()) {
                         logger.debug("[{}] Forwarded msg to new server: {}", sessionId, currentTargetServer.get());
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
index 44eff16..c69946f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -66,7 +66,7 @@ public class SessionManagerActor extends ContextAwareActor {
     }
 
     private void broadcast(Object msg) {
-        sessionActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+        sessionActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
     private void onSessionTimeout(SessionTimeoutMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
index afb35ac..9fb13d3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
@@ -52,7 +52,7 @@ class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
     public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
         if (pendingResponse) {
             try {
-                sessionCtx.onMsg(new SessionCloseMsg(sessionId, true));
+                sessionCtx.onMsg(SessionCloseMsg.onTimeout(sessionId));
             } catch (SessionException e) {
                 logger.warning("Failed to push session close msg", e);
             }
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
index 1c7f687..a3141ee 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -19,7 +19,6 @@ import akka.actor.ActorContext;
 import akka.actor.ActorRef;
 import akka.actor.Scheduler;
 import akka.event.LoggingAdapter;
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.AllArgsConstructor;
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
index c581c41..049accb 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
@@ -15,9 +15,9 @@
  */
 package org.thingsboard.server.actors.shared.plugin;
 
-import java.util.HashMap;
-import java.util.Map;
-
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.plugin.PluginActor;
@@ -29,12 +29,9 @@ import org.thingsboard.server.common.data.page.PageDataIterable;
 import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.dao.plugin.PluginService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-import akka.actor.ActorContext;
-import akka.actor.ActorRef;
-import akka.actor.Props;
+import java.util.HashMap;
+import java.util.Map;
 
 @Slf4j
 public abstract class PluginManager {
@@ -64,17 +61,13 @@ public abstract class PluginManager {
     abstract TenantId getTenantId();
 
     public ActorRef getOrCreatePluginActor(ActorContext context, PluginId pluginId) {
-        ActorRef pluginActor = pluginActors.get(pluginId);
-        if (pluginActor == null) {
-            pluginActor = context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pluginId))
-                    .withDispatcher(DefaultActorService.PLUGIN_DISPATCHER_NAME), pluginId.toString());
-            pluginActors.put(pluginId, pluginActor);
-        }
-        return pluginActor;
+        return pluginActors.computeIfAbsent(pluginId, pId ->
+                context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pId))
+                        .withDispatcher(DefaultActorService.PLUGIN_DISPATCHER_NAME), pId.toString()));
     }
 
     public void broadcast(Object msg) {
-        pluginActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+        pluginActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
     public void remove(PluginId id) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
index d8b58a0..a27e903 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
@@ -20,7 +20,6 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.dao.plugin.BasePluginService;
-import org.thingsboard.server.dao.plugin.PluginService;
 
 public class SystemPluginManager extends PluginManager {
 
@@ -30,7 +29,7 @@ public class SystemPluginManager extends PluginManager {
 
     @Override
     FetchFunction<PluginMetaData> getFetchPluginsFunction() {
-        return link -> pluginService.findSystemPlugins(link);
+        return pluginService::findSystemPlugins;
     }
 
     @Override
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
index 67d44e9..dfe3f44 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
@@ -18,8 +18,7 @@ package org.thingsboard.server.actors.shared.rule;
 import akka.actor.ActorContext;
 import akka.actor.ActorRef;
 import akka.actor.Props;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.rule.RuleActor;
 import org.thingsboard.server.actors.rule.RuleActorChain;
@@ -38,10 +37,9 @@ import org.thingsboard.server.dao.rule.RuleService;
 
 import java.util.*;
 
+@Slf4j
 public abstract class RuleManager {
 
-    protected static final Logger logger = LoggerFactory.getLogger(RuleManager.class);
-
     protected final ActorSystemContext systemContext;
     protected final RuleService ruleService;
     protected final Map<RuleId, ActorRef> ruleActors;
@@ -63,11 +61,11 @@ public abstract class RuleManager {
         ruleMap = new HashMap<>();
 
         for (RuleMetaData rule : ruleIterator) {
-            logger.debug("[{}] Creating rule actor {}", rule.getId(), rule);
+            log.debug("[{}] Creating rule actor {}", rule.getId(), rule);
             ActorRef ref = getOrCreateRuleActor(context, rule.getId());
             RuleActorMetaData actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref);
             ruleMap.put(rule, actorMd);
-            logger.debug("[{}] Rule actor created.", rule.getId());
+            log.debug("[{}] Rule actor created.", rule.getId());
         }
 
         refreshRuleChain();
@@ -79,8 +77,11 @@ public abstract class RuleManager {
             rule = systemContext.getRuleService().findRuleById(ruleId);
         }
         if (rule == null) {
-            rule = ruleMap.keySet().stream().filter(r -> r.getId().equals(ruleId)).findFirst().orElse(null);
-            rule.setState(ComponentLifecycleState.SUSPENDED);
+            rule = ruleMap.keySet().stream()
+                    .filter(r -> r.getId().equals(ruleId))
+                    .peek(r -> r.setState(ComponentLifecycleState.SUSPENDED))
+                    .findFirst()
+                    .orElse(null);
         }
         if (rule != null) {
             RuleActorMetaData actorMd = ruleMap.get(rule);
@@ -92,7 +93,7 @@ public abstract class RuleManager {
             refreshRuleChain();
             return Optional.of(actorMd.getActorRef());
         } else {
-            logger.warn("[{}] Can't process unknown rule!", rule.getId());
+            log.warn("[{}] Can't process unknown rule!", ruleId);
             return Optional.empty();
         }
     }
@@ -100,13 +101,9 @@ public abstract class RuleManager {
     abstract FetchFunction<RuleMetaData> getFetchRulesFunction();
 
     public ActorRef getOrCreateRuleActor(ActorContext context, RuleId ruleId) {
-        ActorRef ruleActor = ruleActors.get(ruleId);
-        if (ruleActor == null) {
-            ruleActor = context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, ruleId))
-                    .withDispatcher(DefaultActorService.RULE_DISPATCHER_NAME), ruleId.toString());
-            ruleActors.put(ruleId, ruleActor);
-        }
-        return ruleActor;
+        return ruleActors.computeIfAbsent(ruleId, rId ->
+                context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, rId))
+                        .withDispatcher(DefaultActorService.RULE_DISPATCHER_NAME), rId.toString()));
     }
 
     public RuleActorChain getRuleChain() {
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
index 6d56832..7fac168 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
@@ -29,7 +29,7 @@ public class SystemRuleManager extends RuleManager {
 
     @Override
     FetchFunction<RuleMetaData> getFetchRulesFunction() {
-        return link -> ruleService.findSystemRules(link);
+        return ruleService::findSystemRules;
     }
 
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index 965c652..c8d5243 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -100,7 +100,7 @@ public class TenantActor extends ContextAwareActor {
 
     private void broadcast(Object msg) {
         pluginManager.broadcast(msg);
-        deviceActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+        deviceActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
     private void onToDeviceActorMsg(ToDeviceActorMsg msg) {
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
index 99bec5b..f1ef6e1 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardMessageConfiguration.java
@@ -18,12 +18,14 @@ package org.thingsboard.server.config;
 import org.springframework.context.MessageSource;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
 import org.springframework.context.support.ResourceBundleMessageSource;
 
 @Configuration
 public class ThingsboardMessageConfiguration {
 
     @Bean
+    @Primary
     public MessageSource messageSource() {
         ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
         messageSource.setBasename("i18n/messages");
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 7ae4604..ef365ca 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -134,7 +134,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
 
     @Override
     protected void configure(HttpSecurity http) throws Exception {
-        http.headers().frameOptions().disable()
+        http.headers().cacheControl().disable().frameOptions().disable()
                 .and()
                 .csrf().disable()
                 .exceptionHandling()
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 1c0a7be..43416ce 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
 
 @RestController
 @RequestMapping("/api")
@@ -48,7 +49,7 @@ public class DeviceController extends BaseController {
 
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
     @RequestMapping(value = "/device", method = RequestMethod.POST)
-    @ResponseBody 
+    @ResponseBody
     public Device saveDevice(@RequestBody Device device) throws ThingsboardException {
         try {
             device.setTenantId(getCurrentUser().getTenantId());
@@ -74,7 +75,7 @@ public class DeviceController extends BaseController {
 
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
     @RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST)
-    @ResponseBody 
+    @ResponseBody
     public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId,
                                          @PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
         checkParameter("customerId", strCustomerId);
@@ -85,7 +86,7 @@ public class DeviceController extends BaseController {
 
             DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
             checkDeviceId(deviceId);
-            
+
             return checkNotNull(deviceService.assignDeviceToCustomer(deviceId, customerId));
         } catch (Exception e) {
             throw handleException(e);
@@ -94,7 +95,7 @@ public class DeviceController extends BaseController {
 
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
     @RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE)
-    @ResponseBody 
+    @ResponseBody
     public Device unassignDeviceFromCustomer(@PathVariable("deviceId") String strDeviceId) throws ThingsboardException {
         checkParameter("deviceId", strDeviceId);
         try {
@@ -125,19 +126,21 @@ public class DeviceController extends BaseController {
 
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
     @RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
-    @ResponseBody 
+    @ResponseBody
     public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
         checkNotNull(deviceCredentials);
         try {
             checkDeviceId(deviceCredentials.getDeviceId());
-            return checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
+            DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials));
+            actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId());
+            return result;
         } catch (Exception e) {
             throw handleException(e);
         }
     }
 
     @PreAuthorize("hasAuthority('TENANT_ADMIN')")
-    @RequestMapping(value = "/tenant/devices", params = { "limit" }, method = RequestMethod.GET)
+    @RequestMapping(value = "/tenant/devices", params = {"limit"}, method = RequestMethod.GET)
     @ResponseBody
     public TextPageData<Device> getTenantDevices(
             @RequestParam int limit,
@@ -154,7 +157,7 @@ public class DeviceController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
-    @RequestMapping(value = "/customer/{customerId}/devices", params = { "limit" }, method = RequestMethod.GET)
+    @RequestMapping(value = "/customer/{customerId}/devices", params = {"limit"}, method = RequestMethod.GET)
     @ResponseBody
     public TextPageData<Device> getCustomerDevices(
             @PathVariable("customerId") String strCustomerId,
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index e06962b..6d0f110 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -33,7 +33,7 @@ import java.util.List;
 @RequestMapping("/api")
 public class WidgetsBundleController extends BaseController {
 
-    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.GET)
     @ResponseBody
     public WidgetsBundle getWidgetsBundleById(@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@@ -76,7 +76,7 @@ public class WidgetsBundleController extends BaseController {
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/widgetsBundles", params = { "limit" }, method = RequestMethod.GET)
     @ResponseBody
     public TextPageData<WidgetsBundle> getWidgetsBundles(
@@ -97,7 +97,7 @@ public class WidgetsBundleController extends BaseController {
         }
     }
 
-    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
     @RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET)
     @ResponseBody
     public List<WidgetsBundle> getWidgetsBundles() throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
index 29e9b3c..dd784f0 100644
--- a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
@@ -166,7 +166,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
     @Override
     public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
         publishCurrentServer();
-        getOtherServers().stream().forEach(
+        getOtherServers().forEach(
                 server -> log.info("Found active server: [{}:{}]", server.getHost(), server.getPort())
         );
     }
@@ -194,13 +194,13 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
         log.info("Processing [{}] event for [{}:{}]", pathChildrenCacheEvent.getType(), instance.getHost(), instance.getPort());
         switch (pathChildrenCacheEvent.getType()) {
             case CHILD_ADDED:
-                listeners.stream().forEach(listener -> listener.onServerAdded(instance));
+                listeners.forEach(listener -> listener.onServerAdded(instance));
                 break;
             case CHILD_UPDATED:
-                listeners.stream().forEach(listener -> listener.onServerUpdated(instance));
+                listeners.forEach(listener -> listener.onServerUpdated(instance));
                 break;
             case CHILD_REMOVED:
-                listeners.stream().forEach(listener -> listener.onServerRemoved(instance));
+                listeners.forEach(listener -> listener.onServerRemoved(instance));
                 break;
         }
     }
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 3c9ecf8..7a3c7ac 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
@@ -135,7 +135,7 @@ public class ConsistentClusterRoutingService implements ClusterRoutingService, D
 
     private void logCircle() {
         log.trace("Consistent Hash Circle Start");
-        circle.entrySet().stream().forEach((e) -> log.debug("{} -> {}", e.getKey(), e.getValue().getServerAddress()));
+        circle.entrySet().forEach((e) -> log.debug("{} -> {}", e.getKey(), e.getValue().getServerAddress()));
         log.trace("Consistent Hash Circle End");
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index a51464c..975b52a 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -31,7 +31,6 @@ import org.thingsboard.server.dao.component.ComponentDescriptorService;
 import org.thingsboard.server.extensions.api.component.*;
 
 import javax.annotation.PostConstruct;
-import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -72,7 +71,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
     }
 
     private void registerComponents(Collection<ComponentDescriptor> comps) {
-        comps.stream().forEach(c -> components.put(c.getClazz(), c));
+        comps.forEach(c -> components.put(c.getClazz(), c));
     }
 
     private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
@@ -119,7 +118,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
                                 throw new RuntimeException("Plugin " + def.getBeanClassName() + "action " + actionClazz.getName() + " has wrong component type!");
                             }
                         }
-                        scannedComponent.setActions(Arrays.asList(pluginAnnotation.actions()).stream().map(action -> action.getName()).collect(Collectors.joining(",")));
+                        scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(action -> action.getName()).collect(Collectors.joining(",")));
                         break;
                     default:
                         throw new RuntimeException(type + " is not supported yet!");
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index b11517c..70949d2 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -26,6 +26,7 @@ import javax.mail.internet.MimeMessage;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.velocity.app.VelocityEngine;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.thingsboard.server.exception.ThingsboardErrorCode;
 import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.common.data.AdminSettings;
@@ -50,6 +51,7 @@ public class DefaultMailService implements MailService {
     private MessageSource messages;
     
     @Autowired
+    @Qualifier("velocityEngine")
     private VelocityEngine engine;
     
     private JavaMailSenderImpl mailSender;
@@ -101,6 +103,11 @@ public class DefaultMailService implements MailService {
             throw new IncorrectParameterException(String.format("Invalid smtp port value: %s", strPort));
         }
     }
+
+    @Override
+    public void sendEmail(String email, String subject, String message) throws ThingsboardException {
+        sendMail(mailSender, mailFrom, email, subject, message);
+    }
     
     @Override
     public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
index e135253..9e44058 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/MailService.java
@@ -22,6 +22,8 @@ import com.fasterxml.jackson.databind.JsonNode;
 public interface MailService {
 
     void updateMailConfiguration();
+
+    void sendEmail(String email, String subject, String message) throws ThingsboardException;
     
     void sendTestMail(JsonNode config, String email) throws ThingsboardException;
     
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 83b87ab..6456968 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
@@ -20,9 +20,9 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.id.UserId;
 
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class SecurityUser extends User {
 
@@ -46,7 +46,7 @@ public class SecurityUser extends User {
 
     public Collection<? extends GrantedAuthority> getAuthorities() {
         if (authorities == null) {
-            authorities = Arrays.asList(SecurityUser.this.getAuthority()).stream()
+            authorities = Stream.of(SecurityUser.this.getAuthority())
                     .map(authority -> new SimpleGrantedAuthority(authority.name()))
                     .collect(Collectors.toList());
         }
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
index 5506578..46c5a38 100644
--- a/application/src/main/resources/logback.xml
+++ b/application/src/main/resources/logback.xml
@@ -25,7 +25,7 @@
         </encoder>
     </appender>
 
-    <logger name="org.thingsboard.server" level="TRACE" />
+    <logger name="org.thingsboard.server" level="INFO" />
     <logger name="akka" level="INFO" />
 
     <root level="INFO">
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index 538e8f9..20ceb77 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -129,8 +129,10 @@ public abstract class AbstractControllerTest {
     @Autowired
     void setConverters(HttpMessageConverter<?>[] converters) {
 
-        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
-                hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
+        this.mappingJackson2HttpMessageConverter = Arrays.stream(converters)
+                .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
+                .findAny()
+                .get();
 
         Assert.assertNotNull("the JSON message converter must not be null",
                 this.mappingJackson2HttpMessageConverter);
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java
index 8d22343..db90b89 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/AbstractFeatureIntegrationTest.java
@@ -61,8 +61,10 @@ public class AbstractFeatureIntegrationTest {
     @Autowired
     void setConverters(HttpMessageConverter<?>[] converters) {
 
-        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
-                hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
+        this.mappingJackson2HttpMessageConverter = Arrays.stream(converters)
+                .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter)
+                .findAny()
+                .get();
 
         assertNotNull("the JSON message converter must not be null",
                 this.mappingJackson2HttpMessageConverter);
diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml
index f32acec..2577ca1 100644
--- a/application/src/test/resources/logback.xml
+++ b/application/src/test/resources/logback.xml
@@ -7,7 +7,7 @@
         </encoder>
     </appender>
 
-    <logger name="org.thingsboard.server" level="DEBUG"/>
+    <logger name="org.thingsboard.server" level="WARN"/>
     <logger name="org.springframework" level="WARN"/>
     <logger name="org.apache.cassandra" level="WARN"/>
     <logger name="org.cassandraunit" level="INFO"/>
diff --git a/common/data/pom.xml b/common/data/pom.xml
index af1da85..6ba5e01 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 5bc54b0..28144f0 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
new file mode 100644
index 0000000..3e96e40
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseNotification.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 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.msg.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+@ToString
+public class SessionCloseNotification implements ToDeviceMsg {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public boolean isSuccess() {
+        return true;
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SESSION_CLOSE;
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
new file mode 100644
index 0000000..d18dc9f
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionOpenMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 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.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class SessionOpenMsg implements FromDeviceMsg {
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SESSION_OPEN;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
index d188527..03b611e 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
@@ -21,11 +21,25 @@ import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
 public class SessionCloseMsg implements SessionCtrlMsg {
 
     private final SessionId sessionId;
+    private final boolean revoked;
     private final boolean timeout;
 
-    public SessionCloseMsg(SessionId sessionId, boolean timeout) {
+    public static SessionCloseMsg onError(SessionId sessionId) {
+        return new SessionCloseMsg(sessionId, false, false);
+    }
+
+    public static SessionCloseMsg onTimeout(SessionId sessionId) {
+        return new SessionCloseMsg(sessionId, false, true);
+    }
+
+    public static SessionCloseMsg onCredentialsRevoked(SessionId sessionId) {
+        return new SessionCloseMsg(sessionId, true, false);
+    }
+
+    private SessionCloseMsg(SessionId sessionId, boolean unauthorized, boolean timeout) {
         super();
         this.sessionId = sessionId;
+        this.revoked = unauthorized;
         this.timeout = timeout;
     }
 
@@ -34,6 +48,10 @@ public class SessionCloseMsg implements SessionCtrlMsg {
         return sessionId;
     }
 
+    public boolean isCredentialsRevoked() {
+        return revoked;
+    }
+
     public boolean isTimeout() {
         return timeout;
     }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
index 1b91425..549a143 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
@@ -28,7 +28,7 @@ public enum MsgType {
 
     RULE_ENGINE_ERROR,
 
-    SESSION_CLOSE;
+    SESSION_OPEN, SESSION_CLOSE;
 
     private final boolean requiresRulesProcessing;
 

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

diff --git a/common/pom.xml b/common/pom.xml
index bdbe1a8..77a44ac 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index b02cc07..8bcd650 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>

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

diff --git a/dao/pom.xml b/dao/pom.xml
index 9ae2cae..de56032 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java
index 4c542e3..ce4dd81 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java
@@ -140,7 +140,7 @@ public class BaseAttributesDao extends AbstractDao implements AttributesDao {
         List<Row> rows = resultSet.all();
         List<AttributeKvEntry> entries = new ArrayList<>(rows.size());
         if (!rows.isEmpty()) {
-            rows.stream().forEach(row -> {
+            rows.forEach(row -> {
                 String key = row.getString(ModelConstants.ATTRIBUTE_KEY_COLUMN);
                 AttributeKvEntry kvEntry = convertResultToAttributesKvEntry(key, row);
                 if (kvEntry != null) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
index 79134f7..851c770 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
@@ -143,7 +143,7 @@ public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao {
     public List<TsKvEntry> convertResultToTsKvEntryList(List<Row> rows) {
         List<TsKvEntry> entries = new ArrayList<>(rows.size());
         if (!rows.isEmpty()) {
-            rows.stream().forEach(row -> {
+            rows.forEach(row -> {
                 TsKvEntry kvEntry = convertResultToTsKvEntry(row);
                 if (kvEntry != null) {
                     entries.add(kvEntry);
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 17784d1..a3ab6bd 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
@@ -156,7 +156,7 @@ public class UserServiceImpl implements UserService {
         UserCredentialsEntity userCredentialsEntity = userCredentialsDao.findByUserId(userEntity.getId());
         UserCredentials userCredentials = getData(userCredentialsEntity);
         if (!userCredentials.isEnabled()) {
-            throw new IncorrectParameterException("Unable to reset password for unactive user");
+            throw new IncorrectParameterException("Unable to reset password for inactive user");
         }
         userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(30));
         return saveUserCredentials(userCredentials);
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index 1358e58..06b9351 100644
--- a/dao/src/main/resources/system-data.cql
+++ b/dao/src/main/resources/system-data.cql
@@ -73,6 +73,11 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'cards', 'simple_card',
 'Simple card' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES (  now ( ), minTimeuuid ( 0 ), 'cards', 'label_widget',
+'{"type":"latest","sizeX":4.5,"sizeY":5,"resources":[],"templateHtml":"","templateCss":"#container {\n    overflow: auto;\n}\n\n.tbDatasource-container {\n    margin: 5px;\n    padding: 8px;\n}\n\n.tbDatasource-title {\n    font-size: 1.200rem;\n    font-weight: 500;\n    padding-bottom: 10px;\n}\n\n.tbDatasource-table {\n    width: 100%;\n    box-shadow: 0 0 10px #ccc;\n    border-collapse: collapse;\n    white-space: nowrap;\n    font-size: 1.000rem;\n    color: #757575;\n}\n\n.tbDatasource-table td {\n    position: relative;\n    border-top: 1px solid rgba(0, 0, 0, 0.12);\n    border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n    padding: 0px 18px;\n    box-sizing: border-box;\n}","controllerScript":"var bImageHeight;\nvar bImageWidth;\nvar backgroundRect;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\nvar labels;\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n\n    var container = $(containerElement);\n    var imageUrl = settings.backgroundImageUrl ? settings.backgroundImageUrl :\n    ''data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg=='';\n\n    container.css(''background'', ''url(\"''+imageUrl+''\") no-repeat'');\n    container.css(''backgroundSize'', ''contain'');\n    container.css(''backgroundPosition'', ''50% 50%'');\n    \n    function processLabelPattern(pattern, data) {\n        var match = varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split('':'');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            \n            if (label.startsWith(''#'')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = n;\n                }\n            }\n            if (variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < data.length; i++) {\n                     var datasourceData = data[i];\n                     var dataKey = datasourceData.dataKey;\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }\n\n    var configuredLabels = settings.labels;\n    if (!configuredLabels) {\n        configuredLabels = [];\n    }\n    \n    labels = [];\n\n    for (var l in configuredLabels) {\n        var labelConfig = configuredLabels[l];\n        var localConfig = {};\n        localConfig.font = {};\n        \n        localConfig.pattern = labelConfig.pattern ? labelConfig.pattern : ''${#0}'';\n        localConfig.x = labelConfig.x ? labelConfig.x : 0;\n        localConfig.y = labelConfig.y ? labelConfig.y : 0;\n        localConfig.backgroundColor = labelConfig.backgroundColor ? labelConfig.backgroundColor : ''rgba(0,0,0,0)'';\n        \n        var settingsFont = labelConfig.font;\n        if (!settingsFont) {\n            settingsFont = {};\n        }\n        \n        localConfig.font.family = settingsFont.family || ''RobotoDraft'';\n        localConfig.font.size = settingsFont.size ? settingsFont.size : 6;\n        localConfig.font.style = settingsFont.style ? settingsFont.style : ''normal'';\n        localConfig.font.weight = settingsFont.weight ? settingsFont.weight : ''500'';\n        localConfig.font.color = settingsFont.color ? settingsFont.color : ''#fff'';\n        \n        localConfig.replaceInfo = processLabelPattern(localConfig.pattern, data);\n        \n        var label = {};\n        var labelElement = $(''<div/>'');\n        labelElement.css(''position'', ''absolute'');\n        labelElement.css(''top'', ''0'');\n        labelElement.css(''left'', ''0'');\n        labelElement.css(''backgroundColor'', localConfig.backgroundColor);\n        labelElement.css(''color'', localConfig.font.color);\n        labelElement.css(''fontFamily'', localConfig.font.family);\n        labelElement.css(''fontStyle'', localConfig.font.style);\n        labelElement.css(''fontWeight'', localConfig.font.weight);\n        \n        labelElement.html(localConfig.pattern);\n        container.append(labelElement);\n        label.element = labelElement;\n        label.config = localConfig;\n        labels.push(label);\n    }\n\n    var bgImg = $(''<img />'');\n    bgImg.hide();\n    bgImg.bind(''load'', function()\n    {\n        bImageHeight = $(this).height();\n        bImageWidth = $(this).width();\n    });\n    container.append(bgImg);\n    bgImg.attr(''src'', imageUrl);\n    \n    units = settings.units || \"\";\n    valueDec = (typeof settings.valueDec !== ''undefined'' && settings.valueDec !== null)\n                ? settings.valueDec : 2;\n                \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n    \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n    \n    function padValue(val, dec, int) {\n        var i = 0;\n        var s, strVal, n;\n    \n        val = parseFloat(val);\n        n = (val < 0);\n        val = Math.abs(val);\n    \n        if (dec > 0) {\n            strVal = val.toFixed(dec).toString().split(''.'');\n            s = int - strVal[0].length;\n    \n            for (; i < s; ++i) {\n                strVal[0] = ''0'' + strVal[0];\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n        }\n    \n        else {\n            strVal = Math.round(val).toString();\n            s = int - strVal.length;\n    \n            for (; i < s; ++i) {\n                strVal = ''0'' + strVal;\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal;\n        }\n    \n        return strVal;\n    }\n    \n    if (bImageHeight && bImageWidth) {\n        if (sizeChanged || !backgroundRect) {\n            backgroundRect = {};\n            var imageRatio = bImageWidth / bImageHeight;\n            var componentRatio = width / height;\n            if (componentRatio >= imageRatio) {\n                backgroundRect.top = 0;\n                backgroundRect.bottom = 1.0;\n                backgroundRect.xRatio = imageRatio / componentRatio;\n                backgroundRect.yRatio = 1;\n                var offset = (1 - backgroundRect.xRatio) / 2;\n                backgroundRect.left = offset;\n                backgroundRect.right = 1 - offset;\n            } else {\n                backgroundRect.left = 0;\n                backgroundRect.right = 1.0;\n                backgroundRect.xRatio = 1;\n                backgroundRect.yRatio = componentRatio / imageRatio;\n                var offset = (1 - backgroundRect.yRatio) / 2;\n                backgroundRect.top = offset;\n                backgroundRect.bottom = 1 - offset;\n            }\n            for (var l in labels) {\n                var label = labels[l];\n                var labelLeft = backgroundRect.left*100 + (label.config.x*backgroundRect.xRatio);\n                var labelTop = backgroundRect.top*100 + (label.config.y*backgroundRect.yRatio);\n                var fontSize = height * backgroundRect.yRatio * label.config.font.size / 100;\n                label.element.css(''top'', labelTop + ''%'');\n                label.element.css(''left'', labelLeft + ''%'');\n                label.element.css(''fontSize'', fontSize + ''px'');\n            }\n            \n        }\n    }\n    \n    for (var l in labels) {\n        var label = labels[l];\n        var text = label.config.pattern;\n        var replaceInfo = label.config.replaceInfo;\n        for (var v in replaceInfo.variables) {\n            var variableInfo = replaceInfo.variables[v];\n            var txtVal = '''';\n            if (variableInfo.dataKeyIndex > -1) {\n                var varData = data[variableInfo.dataKeyIndex].data;\n                if (varData.length > 0) {\n                    var val = varData[varData.length-1][1];\n                    if (isNumber(val)) {\n                        txtVal = padValue(val, variableInfo.valDec, 0);\n                    } else {\n                        txtVal = val;\n                    }\n                }\n            }\n            text = text.split(variableInfo.variable).join(txtVal);\n        }\n        label.element.html(text);\n    }\n    \n\n};\n","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"Settings\",\n        \"required\": [\"backgroundImageUrl\"],\n        \"properties\": {\n            \"backgroundImageUrl\": {\n                \"title\": \"Background image\",\n                \"type\": \"string\",\n                \"default\": \"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\"\n            },\n            \"labels\": {\n                \"title\": \"Labels\",\n                \"type\": \"array\",\n                \"items\": {\n                   \"title\": \"Label\",\n                   \"type\": \"object\",\n                   \"required\": [\"pattern\"],\n                   \"properties\": {\n                       \"pattern\": {\n                           \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units''  )\",\n                           \"type\": \"string\",\n                           \"default\": \"${#0}\"\n                       },\n                       \"x\": {\n                           \"title\": \"X (Percentage relative to background)\",\n                           \"type\": \"number\",\n                           \"default\": 50\n                       },\n                       \"y\": {\n                           \"title\": \"Y (Percentage relative to background)\",\n                           \"type\": \"number\",\n                           \"default\": 50\n                       },\n                       \"backgroundColor\": {\n                           \"title\": \"Backround color\",\n                           \"type\": \"string\",\n                           \"default\": \"rgba(0,0,0,0)\"\n                       },\n                       \"font\": {\n                           \"type\": \"object\",\n                           \"properties\": {\n                               \"family\": {\n                                    \"title\": \"Font family\",\n                                    \"type\": \"string\",\n                                    \"default\": \"RobotoDraft\"\n                                },\n                                \"size\": {\n                                    \"title\": \"Relative font size (percents)\",\n                                    \"type\": \"number\",\n                                    \"default\": 6\n                                },\n                                \"style\": {\n                                    \"title\": \"Style\",\n                                    \"type\": \"string\",\n                                    \"default\": \"normal\"\n                                },\n                                \"weight\": {\n                                    \"title\": \"Weight\",\n                                    \"type\": \"string\",\n                                    \"default\": \"500\"\n                                },\n                                \"color\": {\n                                    \"title\": \"color\",\n                                    \"type\": \"string\",\n                                    \"default\": \"#fff\"\n                                }\n                           }\n                       }\n                   }\n                }\n            }\n        }\n    },\n    \"form\": [\n        {\n            \"key\": \"backgroundImageUrl\",\n            \"type\": \"image\"\n        },\n        {\n            \"key\": \"labels\",\n            \"items\": [\n                \"labels[].pattern\",\n                \"labels[].x\",\n                \"labels[].y\",\n                {\n                    \"key\": \"labels[].backgroundColor\",\n                    \"type\": \"color\"\n                },\n                \"labels[].font.family\",\n                \"labels[].font.size\",\n                {\n                   \"key\": \"labels[].font.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n\n                },\n                {\n                   \"key\": \"labels[].font.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"labels[].font.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        }\n    ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"RobotoDraft\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\"}"}',
+'Label widget' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
 VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'speed_gauge_canvas_gauges',
 '{"type":"latest","sizeX":7,"sizeY":5,"resources":[],"templateHtml":"<canvas id=\"radialGauge\"></canvas>\n","templateCss":"","controllerScript":"var gauge;\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    gauge = new TbAnalogueRadialGauge(containerElement, settings, data, ''radialGauge'');    \n\n}\n\n\nfns.redraw = function(containerElement, width, height, data, timeWindow, sizeChanged) {\n    gauge.redraw(width, height, data, sizeChanged);\n};\n\nfns.destroy = function() {\n}\n","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"Settings\",\n        \"properties\": {\n            \"minValue\": {\n                \"title\": \"Minimum value\",\n                \"type\": \"number\",\n                \"default\": 0\n            },\n            \"maxValue\": {\n                \"title\": \"Maximum value\",\n                \"type\": \"number\",\n                \"default\": 100\n            },\n            \"unitTitle\": {\n                \"title\": \"Unit title\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"showUnitTitle\": {\n                \"title\": \"Show unit title\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"units\": {\n                \"title\": \"Units\",\n                \"type\": \"string\",\n                \"default\": \"\"\n            },\n            \"majorTicksCount\": {\n                \"title\": \"Major ticks count\",\n                \"type\": \"number\",\n                \"default\": null\n            },\n            \"minorTicks\": {\n                \"title\": \"Minor ticks count\",\n                \"type\": \"number\",\n                \"default\": 2\n            },\n            \"valueBox\": {\n                \"title\": \"Show value box\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"valueInt\": {\n                \"title\": \"Digits count for integer part of value\",\n                \"type\": \"number\",\n                \"default\": 3\n            },\n            \"valueDec\": {\n                \"title\": \"Digits count for decimal part of value\",\n                \"type\": \"number\",\n                \"default\": 2\n            },\n            \"defaultColor\": {\n                \"title\": \"Default color\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"colorPlate\": {\n                \"title\": \"Plate color\",\n                \"type\": \"string\",\n                \"default\": \"#fff\"\n            },\n            \"colorMajorTicks\": {\n                \"title\": \"Major ticks color\",\n                \"type\": \"string\",\n                \"default\": \"#444\"\n            },\n            \"colorMinorTicks\": {\n                \"title\": \"Minor ticks color\",\n                \"type\": \"string\",\n                \"default\": \"#666\"\n            },\n            \"colorNeedle\": {\n                \"title\": \"Needle color\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"colorNeedleEnd\": {\n                \"title\": \"Needle color - end gradient\",\n                \"type\": \"string\",\n                \"default\": null\n            },\n            \"colorNeedleShadowUp\": {\n                \"title\": \"Upper half of the needle shadow color\",\n                \"type\": \"string\",\n                \"default\": \"rgba(2,255,255,0.2)\"\n            },\n            \"colorNeedleShadowDown\": {\n                \"title\": \"Drop shadow needle color.\",\n                \"type\": \"string\",\n                \"default\": \"rgba(188,143,143,0.45)\"\n            },\n            \"colorValueBoxRect\": {\n                \"title\": \"Value box rectangle stroke color\",\n                \"type\": \"string\",\n                \"default\": \"#888\"\n            },\n            \"colorValueBoxRectEnd\": {\n                \"title\": \"Value box rectangle stroke color - end gradient\",\n                \"type\": \"string\",\n                \"default\": \"#666\"\n            },\n            \"colorValueBoxBackground\": {\n                \"title\": \"Value box background color\",\n                \"type\": \"string\",\n                \"default\": \"#babab2\"\n            },\n            \"colorValueBoxShadow\": {\n                \"title\": \"Value box shadow color\",\n                \"type\": \"string\",\n                \"default\": \"rgba(0,0,0,1)\"\n            },\n            \"highlights\": {\n                \"title\": \"Highlights\",\n                \"type\": \"array\",\n                \"items\": {\n                  \"title\": \"Highlight\",\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"from\": {\n                      \"title\": \"From\",\n                      \"type\": \"number\"\n                    },\n                    \"to\": {\n                      \"title\": \"To\",\n                      \"type\": \"number\"\n                    },\n                    \"color\": {\n                      \"title\": \"Color\",\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n            },\n            \"highlightsWidth\": {\n                \"title\": \"Highlights width\",\n                \"type\": \"number\",\n                \"default\": 15\n            },\n            \"showBorder\": {\n                \"title\": \"Show border\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"numbersFont\": {\n                \"title\": \"Tick numbers font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 18\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": null\n                    }\n                }\n            },\n            \"titleFont\": {\n                \"title\": \"Title text font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 24\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": \"#888\"\n                    }\n                }\n            },\n            \"unitsFont\": {\n                \"title\": \"Units text font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 22\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": \"#888\"\n                    }\n                }\n            },\n            \"valueFont\": {\n                \"title\": \"Value text font\",\n                \"type\": \"object\",\n                 \"properties\": {\n                    \"family\": {\n                        \"title\": \"Font family\",\n                        \"type\": \"string\",\n                        \"default\": \"RobotoDraft\"\n                    },\n                    \"size\": {\n                      \"title\": \"Size\",\n                      \"type\": \"number\",\n                      \"default\": 40\n                    },\n                    \"style\": {\n                      \"title\": \"Style\",\n                      \"type\": \"string\",\n                      \"default\": \"normal\"\n                    },\n                    \"weight\": {\n                      \"title\": \"Weight\",\n                      \"type\": \"string\",\n                      \"default\": \"500\"\n                    },\n                    \"color\": {\n                        \"title\": \"color\",\n                        \"type\": \"string\",\n                        \"default\": \"#444\"\n                    },\n                    \"shadowColor\": {\n                        \"title\": \"Shadow color\",\n                        \"type\": \"string\",\n                        \"default\": \"rgba(0,0,0,0.3)\"\n                    }\n                }\n            },\n            \"animation\": {\n                \"title\": \"Enable animation\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"animationDuration\": {\n                \"title\": \"Animation duration\",\n                \"type\": \"number\",\n                \"default\": 500\n            },\n            \"animationRule\": {\n                \"title\": \"Animation rule\",\n                \"type\": \"string\",\n                \"default\": \"cycle\"\n            },\n            \"startAngle\": {\n                \"title\": \"Start ticks angle\",\n                \"type\": \"number\",\n                \"default\": 45\n            },\n            \"ticksAngle\": {\n                \"title\": \"Ticks angle\",\n                \"type\": \"number\",\n                \"default\": 270\n            },\n            \"needleCircleSize\": {\n                \"title\": \"Needle circle size\",\n                \"type\": \"number\",\n                \"default\": 10\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"startAngle\",\n        \"ticksAngle\",\n        \"needleCircleSize\",\n        \"minValue\",\n        \"maxValue\",\n        \"unitTitle\",\n        \"showUnitTitle\",\n        \"units\",\n        \"majorTicksCount\",\n        \"minorTicks\",\n        \"valueBox\",\n        \"valueInt\",\n        \"valueDec\",\n        {\n            \"key\": \"defaultColor\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorPlate\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorMajorTicks\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorMinorTicks\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedle\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedleEnd\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedleShadowUp\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorNeedleShadowDown\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxRect\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxRectEnd\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxBackground\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"colorValueBoxShadow\",\n            \"type\": \"color\"\n        },\n        {\n            \"key\": \"highlights\",\n            \"items\": [\n                \"highlights[].from\",\n                \"highlights[].to\",\n                {\n                    \"key\": \"highlights[].color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        \"highlightsWidth\",\n        \"showBorder\",\n        {\n            \"key\": \"numbersFont\",\n            \"items\": [\n                \"numbersFont.family\",\n                \"numbersFont.size\",\n                {\n                   \"key\": \"numbersFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"numbersFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"numbersFont.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        {\n            \"key\": \"titleFont\",\n            \"items\": [\n                \"titleFont.family\",\n                \"titleFont.size\",\n                {\n                   \"key\": \"titleFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"titleFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"titleFont.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        {\n            \"key\": \"unitsFont\",\n            \"items\": [\n                \"unitsFont.family\",\n                \"unitsFont.size\",\n                {\n                   \"key\": \"unitsFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"unitsFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"unitsFont.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        {\n            \"key\": \"valueFont\",\n            \"items\": [\n                \"valueFont.family\",\n                \"valueFont.size\",\n                {\n                   \"key\": \"valueFont.style\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"italic\",\n                           \"label\": \"Italic\"\n                       },\n                       {\n                           \"value\": \"oblique\",\n                           \"label\": \"Oblique\"\n                       }\n                    ]\n                },\n                {\n                   \"key\": \"valueFont.weight\",\n                   \"type\": \"rc-select\",\n                   \"multiple\": false,\n                   \"items\": [\n                       {\n                           \"value\": \"normal\",\n                           \"label\": \"Normal\"\n                       },\n                       {\n                           \"value\": \"bold\",\n                           \"label\": \"Bold\"\n                       },\n                       {\n                           \"value\": \"bolder\",\n                           \"label\": \"Bolder\"\n                       },\n                       {\n                           \"value\": \"lighter\",\n                           \"label\": \"Lighter\"\n                       },\n                       {\n                           \"value\": \"100\",\n                           \"label\": \"100\"\n                       },\n                       {\n                           \"value\": \"200\",\n                           \"label\": \"200\"\n                       },\n                       {\n                           \"value\": \"300\",\n                           \"label\": \"300\"\n                       },\n                       {\n                           \"value\": \"400\",\n                           \"label\": \"400\"\n                       },\n                       {\n                           \"value\": \"500\",\n                           \"label\": \"500\"\n                       },\n                       {\n                           \"value\": \"600\",\n                           \"label\": \"600\"\n                       },\n                       {\n                           \"value\": \"700\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"800\",\n                           \"label\": \"800\"\n                       },\n                       {\n                           \"value\": \"900\",\n                           \"label\": \"900\"\n                       }\n                    ]\n                },\n                {\n                    \"key\": \"valueFont.color\",\n                    \"type\": \"color\"\n                },\n                {\n                    \"key\": \"valueFont.shadowColor\",\n                    \"type\": \"color\"\n                }\n            ]\n        },        \n        \"animation\",\n        \"animationDuration\",\n        {\n            \"key\": \"animationRule\",\n            \"type\": \"rc-select\",\n            \"multiple\": false,\n            \"items\": [\n                {\n                    \"value\": \"linear\",\n                    \"label\": \"Linear\"\n                },\n                {\n                    \"value\": \"quad\",\n                    \"label\": \"Quad\"\n                },\n                {\n                    \"value\": \"quint\",\n                    \"label\": \"Quint\"\n                },\n                {\n                    \"value\": \"cycle\",\n                    \"label\": \"Cycle\"\n                },\n                {\n                    \"value\": \"bounce\",\n                    \"label\": \"Bounce\"\n                },\n                {\n                    \"value\": \"elastic\",\n                    \"label\": \"Elastic\"\n                },\n                {\n                    \"value\": \"dequad\",\n                    \"label\": \"Dequad\"\n                },\n                {\n                    \"value\": \"dequint\",\n                    \"label\": \"Dequint\"\n                },\n                {\n                    \"value\": \"decycle\",\n                    \"label\": \"Decycle\"\n                },\n                {\n                    \"value\": \"debounce\",\n                    \"label\": \"Debounce\"\n                },\n                {\n                    \"value\": \"delastic\",\n                    \"label\": \"Delastic\"\n                }\n            ]\n        }\n    ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Speed\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.7282710489093589,\"funcBody\":\"var value = prevValue + Math.random() * 50 - 25;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 220) {\\n\\tvalue = 220;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"maxValue\":180,\"startAngle\":45,\"ticksAngle\":270,\"showBorder\":false,\"defaultColor\":\"#e65100\",\"needleCircleSize\":7,\"highlights\":[{\"from\":80,\"to\":120,\"color\":\"#fdd835\"},{\"color\":\"#e57373\",\"from\":120,\"to\":180}],\"showUnitTitle\":false,\"colorPlate\":\"#fff\",\"colorMajorTicks\":\"#444\",\"colorMinorTicks\":\"#666\",\"minorTicks\":2,\"valueInt\":3,\"minValue\":0,\"valueDec\":0,\"highlightsWidth\":15,\"valueBox\":true,\"animation\":true,\"animationDuration\":1500,\"animationRule\":\"linear\",\"colorNeedleShadowUp\":\"rgba(2, 255, 255, 0)\",\"colorNeedleShadowDown\":\"rgba(188, 143, 143, 0.78)\",\"units\":\"MPH\",\"majorTicksCount\":9,\"numbersFont\":{\"family\":\"RobotoDraft\",\"size\":22,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"titleFont\":{\"family\":\"RobotoDraft\",\"size\":24,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#888\"},\"unitsFont\":{\"family\":\"RobotoDraft\",\"size\":28,\"style\":\"normal\",\"weight\":\"500\",\"color\":\"#616161\"},\"valueFont\":{\"size\":32,\"style\":\"normal\",\"weight\":\"normal\",\"shadowColor\":\"rgba(0, 0, 0, 0.49)\",\"color\":\"#444\",\"family\":\"Segment7Standard\"},\"colorValueBoxRect\":\"#888\",\"colorValueBoxRectEnd\":\"#666\",\"colorValueBoxBackground\":\"#babab2\",\"colorValueBoxShadow\":\"rgba(0,0,0,1)\"},\"title\":\"Speed gauge - Canvas Gauges\"}"}',
 'Speed gauge - Canvas Gauges' );
@@ -123,11 +128,16 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'analogue_gauges', 'radial_gauge_canvas_gau
 'Radial gauge - Canvas Gauges' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
-VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
-'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n    color: red;\n}\n.tb-labels {\n  color: #222;\n  font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n  text-align: center;\n  width: 100px;\n  white-space: nowrap;\n}","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar markerCluster;\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    var configuredMarkersSettings = settings.markersSettings;\n    if (!configuredMarkersSettings) {\n        configuredMarkersSettings = [];\n    }\n    \n    for (var i=0;i<datasources.length;i++) {\n        markersSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,\n            color: \"FE7569\"\n        };\n        if (configuredMarkersSettings[i]) {\n            markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n            markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n            markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n            markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n            markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n        }\n    }\n\n    var mapId = '''' + Math.random().toString(36).substr(2, 9);\n    \n    function clearGlobalId() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n        }\n    }\n    \n    $window.gm_authFailure = function() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n            $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n            displayError($window.gmApiKeys[apiKey].error);\n        }\n    };\n    \n    function displayError(message) {\n        $(containerElement).html(\n            \"<div class=''error''>\"+ message + \"</div>\"\n        );\n    }\n\n    var initMapFunctionName = ''initGoogleMap_'' + mapId;\n    $window[initMapFunctionName] = function() {\n        lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n            function success() {\n                initMap();\n            },\n            function fail() {\n                clearGloabalId();\n                $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                displayError($window.gmApiKeys[apiKey].error);\n            }\n        );\n        \n    };   \n    \n    var apiKey = settings.gmApiKey || '''';\n\n    if (apiKey && apiKey.length > 0) {\n        if (!$window.gmApiKeys) {\n            $window.gmApiKeys = {};\n        }\n        if ($window.gmApiKeys[apiKey]) {\n            if ($window.gmApiKeys[apiKey].error) {\n                displayError($window.gmApiKeys[apiKey].error);\n            } else {\n                initMap();\n            }\n        } else {\n            $window.gmApiKeys[apiKey] = {};\n            var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n        \n            $window.loadingGmId = mapId;\n            lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n                function success() {\n                    setTimeout(clearGlobalId, 2000);\n                },\n                function fail(e) {\n                    clearGloabalId();\n                    $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                    displayError($window.gmApiKeys[apiKey].error);\n                }\n            );\n        }\n    } else {\n        displayError(''No Google Map Api Key provided!'');\n    }\n\n    function initMap() {\n        \n        map = new google.maps.Map(containerElement, {\n          scrollwheel: false,\n          zoom: defaultZoomLevel || 8\n        });\n\n    };\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n    \n    function createMarker(location, settings) {\n        var pinColor = settings.color;\n        var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n            new google.maps.Size(21, 34),\n            new google.maps.Point(0,0),\n            new google.maps.Point(10, 34));\n        var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n            new google.maps.Size(40, 37),\n            new google.maps.Point(0, 0),\n            new google.maps.Point(12, 35));        \n        var marker;\n        if (settings.showLabel) {    \n                marker = new MarkerWithLabel({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow,\n                    labelContent: ''<b>''+settings.label+''</b>'',\n                    labelClass: \"tb-labels\",\n                    labelAnchor: new google.maps.Point(50, 55)\n                });            \n        } else {\n                marker = new google.maps.Marker({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow\n                });            \n        }\n            \n        return marker;    \n    }\n    \n    function updatePosition(position, data) {\n        if (position.latIndex > -1 && position.lngIndex > -1) {\n            var latData = data[position.latIndex].data;\n            var lngData = data[position.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var lat = latData[latData.length-1][1];\n                var lng = lngData[lngData.length-1][1];\n                var location = new google.maps.LatLng(lat, lng);\n                if (!position.marker) {\n                    position.marker = createMarker(location, position.settings);\n                    markers.push(position.marker);\n                    return true;\n                } else {\n                    var prevPosition = position.marker.getPosition();\n                    if (!prevPosition.equals(location)) {\n                        position.marker.setPosition(location);\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n        \n    function loadPositions(data) {\n        var bounds = new google.maps.LatLngBounds();\n        positions = [];\n        var datasourceIndex = -1;\n        var markerSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                markerSettings = markersSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === markerSettings.latKeyName ||\n                dataKey.label === markerSettings.lngKeyName) {\n                var position = positions[datasourceIndex];\n                if (!position) {\n                    position = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: markerSettings\n                    };\n                    positions[datasourceIndex] = position;\n                } else if (position.marker) {\n                    continue;\n                }\n                if (dataKey.label === markerSettings.latKeyName) {\n                    position.latIndex = i;\n                } else {\n                    position.lngIndex = i;\n                }\n                if (position.latIndex > -1 && position.lngIndex > -1) {\n                    updatePosition(position, data);\n                    if (position.marker) {\n                        bounds.extend(position.marker.getPosition());\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n    \n    function updatePositions(data) {\n        var positionsChanged = false;\n        var bounds = new google.maps.LatLngBounds();\n        for (var p in positions) {\n            var position = positions[p];\n            positionsChanged |= updatePosition(position, data);\n            if (position.marker) {\n                bounds.extend(position.marker.getPosition());\n            }\n        }\n        if (!dontFitMapBounds && positionsChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n            var zoomLevel = defaultZoomLevel || map.getZoom();\n            this.setZoom(zoomLevel);\n            if (!defaultZoomLevel && this.getZoom() > 15) {\n                this.setZoom(15);\n            }\n        });\n        map.fitBounds(bounds);\n    }\n\n    if (map) {\n        if (data) {\n            if (!positions) {\n                loadPositions(data);\n            } else {\n                updatePositions(data);\n            }\n        }\n        if (sizeChanged) {\n            google.maps.event.trigger(map, \"resize\");\n            var bounds = new google.maps.LatLngBounds();\n            for (var m in markers) {\n                bounds.extend(markers[m].getPosition());\n            }\n            fitMapBounds(bounds);\n        }\n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Google Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"gmApiKey\": {\n        \"title\": \"Google Maps API Key\",\n        \"type\": \"string\"\n      },\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all markers\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"markersSettings\": {\n            \"title\": \"Markers settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Marker settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },                  \n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n      \"gmApiKey\"\n    ]\n  },\n  \"form\": [\n    \"gmApiKey\",\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"markersSettings\",\n        \"items\": [\n            \"markersSettings[].latKeyName\",\n            \"markersSettings[].lngKeyName\",\n            \"markersSettings[].showLabel\",\n            \"markersSettings[].label\",\n            {\n                \"key\": \"markersSettings[].color\",\n                \"type\": \"color\"\n            }\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"Google Maps\"}"}',
+VALUES (  now ( ), minTimeuuid ( 0 ), 'maps', 'google_maps',
+'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n    color: red;\n}\n.tb-labels {\n  color: #222;\n  font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n  text-align: center;\n  width: 100px;\n  white-space: nowrap;\n}","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n        var match = varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split('':'');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            \n            if (label.startsWith(''#'')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = datasourceOffset + n;\n                }\n            }\n            if (variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < datasource.dataKeys.length; i++) {\n                     var dataKey = datasource.dataKeys[i];\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = datasourceOffset + i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }\n    \n    var configuredMarkersSettings = settings.markersSettings;\n    if (!configuredMarkersSettings) {\n        configuredMarkersSettings = [];\n    }\n    \n    var datasourceOffset = 0;\n    for (var i=0;i<datasources.length;i++) {\n        markersSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,\n            color: \"FE7569\",\n            tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n        };\n        if (configuredMarkersSettings[i]) {\n            markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n            markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n            \n            markersSettings[i].tooltipPattern = configuredMarkersSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+markersSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+markersSettings[i].lngKeyName+\":7}\";\n            \n            markersSettings[i].tooltipReplaceInfo = procesTooltipPattern(markersSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n            \n            markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n            markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n            markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n        }\n        datasourceOffset += datasources[i].dataKeys.length;\n    }\n\n    var mapId = '''' + Math.random().toString(36).substr(2, 9);\n    \n    function clearGlobalId() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n        }\n    }\n    \n    $window.gm_authFailure = function() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n            $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n            displayError($window.gmApiKeys[apiKey].error);\n        }\n    };\n    \n    function displayError(message) {\n        $(containerElement).html(\n            \"<div class=''error''>\"+ message + \"</div>\"\n        );\n    }\n\n    var initMapFunctionName = ''initGoogleMap_'' + mapId;\n    $window[initMapFunctionName] = function() {\n        lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n            function success() {\n                initMap();\n            },\n            function fail() {\n                clearGloabalId();\n                $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                displayError($window.gmApiKeys[apiKey].error);\n            }\n        );\n        \n    };   \n    \n    var apiKey = settings.gmApiKey || '''';\n\n    if (apiKey && apiKey.length > 0) {\n        if (!$window.gmApiKeys) {\n            $window.gmApiKeys = {};\n        }\n        if ($window.gmApiKeys[apiKey]) {\n            if ($window.gmApiKeys[apiKey].error) {\n                displayError($window.gmApiKeys[apiKey].error);\n            } else {\n                initMap();\n            }\n        } else {\n            $window.gmApiKeys[apiKey] = {};\n            var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n        \n            $window.loadingGmId = mapId;\n            lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n                function success() {\n                    setTimeout(clearGlobalId, 2000);\n                },\n                function fail(e) {\n                    clearGloabalId();\n                    $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                    displayError($window.gmApiKeys[apiKey].error);\n                }\n            );\n        }\n    } else {\n        displayError(''No Google Map Api Key provided!'');\n    }\n\n    function initMap() {\n        \n        map = new google.maps.Map(containerElement, {\n          scrollwheel: false,\n          zoom: defaultZoomLevel || 8\n        });\n\n    };\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n    \n    \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n    \n    function padValue(val, dec, int) {\n        var i = 0;\n        var s, strVal, n;\n    \n        val = parseFloat(val);\n        n = (val < 0);\n        val = Math.abs(val);\n    \n        if (dec > 0) {\n            strVal = val.toFixed(dec).toString().split(''.'');\n            s = int - strVal[0].length;\n    \n            for (; i < s; ++i) {\n                strVal[0] = ''0'' + strVal[0];\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n        }\n    \n        else {\n            strVal = Math.round(val).toString();\n            s = int - strVal.length;\n    \n            for (; i < s; ++i) {\n                strVal = ''0'' + strVal;\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal;\n        }\n    \n        return strVal;\n    }            \n    \n    function createMarker(location, settings) {\n        var pinColor = settings.color;\n        var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n            new google.maps.Size(21, 34),\n            new google.maps.Point(0,0),\n            new google.maps.Point(10, 34));\n        var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n            new google.maps.Size(40, 37),\n            new google.maps.Point(0, 0),\n            new google.maps.Point(12, 35));        \n        var marker;\n        if (settings.showLabel) {    \n                marker = new MarkerWithLabel({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow,\n                    labelContent: ''<b>''+settings.label+''</b>'',\n                    labelClass: \"tb-labels\",\n                    labelAnchor: new google.maps.Point(50, 55)\n                });            \n        } else {\n                marker = new google.maps.Marker({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow\n                });            \n        }\n        \n        createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n            \n        return marker;    \n    }\n    \n    function createTooltip(marker, pattern, replaceInfo) {\n        var infowindow = new google.maps.InfoWindow({\n          content: ''''\n        });\n        marker.addListener(''click'', function() {\n          infowindow.open(map, marker);\n        });\n        tooltips.push( {\n            infowindow: infowindow,\n            pattern: pattern,\n            replaceInfo: replaceInfo\n        });\n    }\n    \n    function updatePosition(position, data) {\n        if (position.latIndex > -1 && position.lngIndex > -1) {\n            var latData = data[position.latIndex].data;\n            var lngData = data[position.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var lat = latData[latData.length-1][1];\n                var lng = lngData[lngData.length-1][1];\n                var location = new google.maps.LatLng(lat, lng);\n                if (!position.marker) {\n                    position.marker = createMarker(location, position.settings);\n                    markers.push(position.marker);\n                    return true;\n                } else {\n                    var prevPosition = position.marker.getPosition();\n                    if (!prevPosition.equals(location)) {\n                        position.marker.setPosition(location);\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n        \n    function loadPositions(data) {\n        var bounds = new google.maps.LatLngBounds();\n        positions = [];\n        var datasourceIndex = -1;\n        var markerSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                markerSettings = markersSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === markerSettings.latKeyName ||\n                dataKey.label === markerSettings.lngKeyName) {\n                var position = positions[datasourceIndex];\n                if (!position) {\n                    position = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: markerSettings\n                    };\n                    positions[datasourceIndex] = position;\n                } else if (position.marker) {\n                    continue;\n                }\n                if (dataKey.label === markerSettings.latKeyName) {\n                    position.latIndex = i;\n                } else {\n                    position.lngIndex = i;\n                }\n                if (position.latIndex > -1 && position.lngIndex > -1) {\n                    updatePosition(position, data);\n                    if (position.marker) {\n                        bounds.extend(position.marker.getPosition());\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n    \n    function updatePositions(data) {\n        var positionsChanged = false;\n        var bounds = new google.maps.LatLngBounds();\n        for (var p in positions) {\n            var position = positions[p];\n            positionsChanged |= updatePosition(position, data);\n            if (position.marker) {\n                bounds.extend(position.marker.getPosition());\n            }\n        }\n        if (!dontFitMapBounds && positionsChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n            var zoomLevel = defaultZoomLevel || map.getZoom();\n            this.setZoom(zoomLevel);\n            if (!defaultZoomLevel && this.getZoom() > 15) {\n                this.setZoom(15);\n            }\n        });\n        map.fitBounds(bounds);\n    }\n\n    if (map) {\n        if (data) {\n            if (!positions) {\n                loadPositions(data);\n            } else {\n                updatePositions(data);\n            }\n        }\n        if (sizeChanged) {\n            google.maps.event.trigger(map, \"resize\");\n            if (!dontFitMapBounds) {\n                var bounds = new google.maps.LatLngBounds();\n                for (var m in markers) {\n                    bounds.extend(markers[m].getPosition());\n                }\n                fitMapBounds(bounds);\n            }\n        }\n        \n        for (var t in tooltips) {\n            var tooltip = tooltips[t];\n            var text = tooltip.pattern;\n            var replaceInfo = tooltip.replaceInfo;\n            for (var v in replaceInfo.variables) {\n                var variableInfo = replaceInfo.variables[v];\n                var txtVal = '''';\n                if (variableInfo.dataKeyIndex > -1) {\n                    var varData = data[variableInfo.dataKeyIndex].data;\n                    if (varData.length > 0) {\n                        var val = varData[varData.length-1][1];\n                        if (isNumber(val)) {\n                            txtVal = padValue(val, variableInfo.valDec, 0);\n                        } else {\n                            txtVal = val;\n                        }\n                    }\n                }\n                text = text.split(variableInfo.variable).join(txtVal);\n            }\n            tooltip.infowindow.setContent(text);\n        }        \n        \n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Google Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"gmApiKey\": {\n        \"title\": \"Google Maps API Key\",\n        \"type\": \"string\"\n      },\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all markers\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"markersSettings\": {\n            \"title\": \"Markers settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Marker settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },                  \n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"tooltipPattern\": {\n                    \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units''  )\",\n                    \"type\": \"string\",\n                    \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n      \"gmApiKey\"\n    ]\n  },\n  \"form\": [\n    \"gmApiKey\",\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"markersSettings\",\n        \"items\": [\n            \"markersSettings[].latKeyName\",\n            \"markersSettings[].lngKeyName\",\n            \"markersSettings[].showLabel\",\n            \"markersSettings[].label\",\n            \"markersSettings[].tooltipPattern\",\n            {\n                \"key\": \"markersSettings[].color\",\n                \"type\": \"color\"\n            }\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"}],\"fitMapBounds\":true},\"title\":\"Google Maps\"}"}',
 'Google Maps' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
+VALUES (  now ( ), minTimeuuid ( 0 ), 'maps', 'route_map',
+'{"type":"timeseries","sizeX":8.5,"sizeY":6,"resources":[],"templateHtml":"","templateCss":".error {\n    color: red;\n}\n.tb-labels {\n  color: #222;\n  font: 12px/1.5 \"Helvetica Neue\", Arial, Helvetica, sans-serif;\n  text-align: center;\n  width: 100px;\n  white-space: nowrap;\n}","controllerScript":"var map;\n\nvar routesSettings = [];\nvar routes;\nvar polylines = [];\n\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n        var match = varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split('':'');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            \n            if (label.startsWith(''#'')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = datasourceOffset + n;\n                }\n            }\n            if (variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < datasource.dataKeys.length; i++) {\n                     var dataKey = datasource.dataKeys[i];\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = datasourceOffset + i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }\n\n    \n    var configuredRoutesSettings = settings.routesSettings;\n    if (!configuredRoutesSettings) {\n        configuredRoutesSettings = [];\n    }\n    \n    var datasourceOffset = 0;\n    for (var i=0;i<datasources.length;i++) {\n        routesSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,            \n            color: \"#FE7569\",\n            strokeWeight: 2,\n            strokeOpacity: 1.0,\n            tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n        };\n        if (configuredRoutesSettings[i]) {\n            routesSettings[i].latKeyName = configuredRoutesSettings[i].latKeyName || routesSettings[i].latKeyName;\n            routesSettings[i].lngKeyName = configuredRoutesSettings[i].lngKeyName || routesSettings[i].lngKeyName;\n            routesSettings[i].tooltipPattern = configuredRoutesSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+routesSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+routesSettings[i].lngKeyName+\":7}\";\n            \n            routesSettings[i].tooltipReplaceInfo = procesTooltipPattern(routesSettings[i].tooltipPattern, datasources[i], datasourceOffset);\n            \n            routesSettings[i].showLabel = configuredRoutesSettings[i].showLabel !== false;\n            routesSettings[i].label = configuredRoutesSettings[i].label || routesSettings[i].label;\n            routesSettings[i].color = configuredRoutesSettings[i].color ? tinycolor(configuredRoutesSettings[i].color).toHexString() : routesSettings[i].color;\n            routesSettings[i].strokeWeight = configuredRoutesSettings[i].strokeWeight || routesSettings[i].strokeWeight;\n            routesSettings[i].strokeOpacity = typeof configuredRoutesSettings[i].strokeOpacity !== \"undefined\" ? configuredRoutesSettings[i].strokeOpacity : routesSettings[i].strokeOpacity;            \n        }\n        datasourceOffset += datasources[i].dataKeys.length;\n    }\n\n    var mapId = '''' + Math.random().toString(36).substr(2, 9);\n    \n    function clearGlobalId() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n        }\n    }\n    \n    $window.gm_authFailure = function() {\n        if ($window.loadingGmId && $window.loadingGmId === mapId) {\n            $window.loadingGmId = null;\n            $window.gmApiKeys[apiKey].error = ''Unable to authentificate for Google Map API.</br>Please check your API key.'';\n            displayError($window.gmApiKeys[apiKey].error);\n        }\n    };\n    \n    function displayError(message) {\n        $(containerElement).html(\n            \"<div class=''error''>\"+ message + \"</div>\"\n        );\n    }\n\n    var initMapFunctionName = ''initGoogleMap_'' + mapId;\n    $window[initMapFunctionName] = function() {\n        lazyLoad.load({ type: ''js'', path: ''https://cdn.rawgit.com/googlemaps/v3-utility-library/master/markerwithlabel/src/markerwithlabel.js'' }).then(\n            function success() {\n                initMap();\n            },\n            function fail() {\n                clearGloabalId();\n                $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                displayError($window.gmApiKeys[apiKey].error);\n            }\n        );\n        \n    };   \n    \n    var apiKey = settings.gmApiKey || '''';\n\n    if (apiKey && apiKey.length > 0) {\n        if (!$window.gmApiKeys) {\n            $window.gmApiKeys = {};\n        }\n        if ($window.gmApiKeys[apiKey]) {\n            if ($window.gmApiKeys[apiKey].error) {\n                displayError($window.gmApiKeys[apiKey].error);\n            } else {\n                initMap();\n            }\n        } else {\n            $window.gmApiKeys[apiKey] = {};\n            var googleMapScriptRes = ''https://maps.googleapis.com/maps/api/js?key=''+apiKey+''&callback=''+initMapFunctionName;\n        \n            $window.loadingGmId = mapId;\n            lazyLoad.load({ type: ''js'', path: googleMapScriptRes }).then(\n                function success() {\n                    setTimeout(clearGlobalId, 2000);\n                },\n                function fail(e) {\n                    clearGloabalId();\n                    $window.gmApiKeys[apiKey].error = ''Google map api load failed!</br>''+e;\n                    displayError($window.gmApiKeys[apiKey].error);\n                }\n            );\n        }\n    } else {\n        displayError(''No Google Map Api Key provided!'');\n    }\n\n    function initMap() {\n        \n        map = new google.maps.Map(containerElement, {\n          scrollwheel: false,\n          zoom: defaultZoomLevel || 8\n        });\n\n    }\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n        \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n    \n    function padValue(val, dec, int) {\n        var i = 0;\n        var s, strVal, n;\n    \n        val = parseFloat(val);\n        n = (val < 0);\n        val = Math.abs(val);\n    \n        if (dec > 0) {\n            strVal = val.toFixed(dec).toString().split(''.'');\n            s = int - strVal[0].length;\n    \n            for (; i < s; ++i) {\n                strVal[0] = ''0'' + strVal[0];\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n        }\n    \n        else {\n            strVal = Math.round(val).toString();\n            s = int - strVal.length;\n    \n            for (; i < s; ++i) {\n                strVal = ''0'' + strVal;\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal;\n        }\n    \n        return strVal;\n    }        \n        \n    function createMarker(location, settings) {\n        var pinColor = settings.color.substr(1);\n        var pinImage = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|\" + pinColor,\n            new google.maps.Size(21, 34),\n            new google.maps.Point(0,0),\n            new google.maps.Point(10, 34));\n        var pinShadow = new google.maps.MarkerImage(\"http://chart.apis.google.com/chart?chst=d_map_pin_shadow\",\n            new google.maps.Size(40, 37),\n            new google.maps.Point(0, 0),\n            new google.maps.Point(12, 35));        \n        var marker;\n        if (settings.showLabel) {    \n                marker = new MarkerWithLabel({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow,\n                    labelContent: ''<b>''+settings.label+''</b>'',\n                    labelClass: \"tb-labels\",\n                    labelAnchor: new google.maps.Point(50, 55)\n                });            \n        } else {\n                marker = new google.maps.Marker({\n                    position: location, \n                    map: map,\n                    icon: pinImage,\n                    shadow: pinShadow\n                });            \n        }\n        \n        createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n            \n        return marker;    \n    }\n    \n    function createTooltip(marker, pattern, replaceInfo) {\n        var infowindow = new google.maps.InfoWindow({\n          content: ''''\n        });\n        marker.addListener(''click'', function() {\n          infowindow.open(map, marker);\n        });\n        tooltips.push( {\n            infowindow: infowindow,\n            pattern: pattern,\n            replaceInfo: replaceInfo\n        });\n    }\n\n    function createPolyline(locations, settings) {\n        var polyline = new google.maps.Polyline({\n          path: locations,\n          strokeColor: settings.color,\n          strokeOpacity: settings.strokeOpacity,\n          strokeWeight: settings.strokeWeight,\n          map: map\n        });\n            \n        return polyline;    \n    }    \n    \n    function arraysEqual(a, b) {\n        if (a === b) return true;\n        if (a === null || b === null) return false;\n        if (a.length != b.length) return false;\n\n        for (var i = 0; i < a.length; ++i) {\n            if (a[i] !== b[i]) return false;\n        }\n        return true;\n    }\n    \n    \n    function updateRoute(route, data) {\n        if (route.latIndex > -1 && route.lngIndex > -1) {\n            var latData = data[route.latIndex].data;\n            var lngData = data[route.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var locations = [];\n                for (var i = 0; i < latData.length; i++) {\n                    var lat = latData[i][1];\n                    var lng = lngData[i][1];\n                    var location = new google.maps.LatLng(lat, lng);\n                    locations.push(location);\n                }\n                var markerLocation;\n                if (locations.length > 0) {\n                    markerLocation = locations[locations.length-1];\n                }\n                if (!route.polyline) {\n                    route.polyline = createPolyline(locations, route.settings);\n                    if (markerLocation) {\n                        route.marker = createMarker(markerLocation, route.settings);\n                    }\n                    polylines.push(route.polyline);\n                    return true;\n                } else {\n                    var prevPath = route.polyline.getPath();\n                    if (!prevPath || !arraysEqual(prevPath.getArray(), locations)) {\n                        route.polyline.setPath(locations);\n                        if (markerLocation) {\n                            if (!route.marker) {\n                                route.marker = createMarker(markerLocation, route.settings);\n                            } else {\n                                route.marker.setPosition(markerLocation);\n                            }\n                        }\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n    \n    function extendBounds(bounds, polyline) {\n        if (polyline && polyline.getPath()) {\n            var locations = polyline.getPath();\n            for (var i = 0; i < locations.getLength(); i++) {\n                bounds.extend(locations.getAt(i));\n            }\n        }\n    }\n    \n    function loadRoutes(data) {\n        var bounds = new google.maps.LatLngBounds();\n        routes = [];\n        var datasourceIndex = -1;\n        var routeSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                routeSettings = routesSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === routeSettings.latKeyName ||\n                dataKey.label === routeSettings.lngKeyName) {\n                var route = routes[datasourceIndex];\n                if (!route) {\n                    route = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: routeSettings\n                    };\n                    routes[datasourceIndex] = route;\n                } else if (route.polyline) {\n                    continue;\n                }\n                if (dataKey.label === routeSettings.latKeyName) {\n                    route.latIndex = i;\n                } else {\n                    route.lngIndex = i;\n                }\n                if (route.latIndex > -1 && route.lngIndex > -1) {\n                    updateRoute(route, data);\n                    if (route.polyline) {\n                        extendBounds(bounds, route.polyline);\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n \n    \n    function updateRoutes(data) {\n        var routesChanged = false;\n        var bounds = new google.maps.LatLngBounds();\n        for (var r in routes) {\n            var route = routes[r];\n            routesChanged |= updateRoute(route, data);\n            if (route.polyline) {\n                extendBounds(bounds, route.polyline);\n            }\n        }\n        if (!dontFitMapBounds && routesChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        google.maps.event.addListenerOnce(map, ''bounds_changed'', function(event) {\n            var zoomLevel = defaultZoomLevel || map.getZoom();\n            this.setZoom(zoomLevel);\n            if (!defaultZoomLevel && this.getZoom() > 15) {\n                this.setZoom(15);\n            }\n        });\n        map.fitBounds(bounds);\n    }\n\n    if (map) {\n        if (data) {\n            if (!routes) {\n                loadRoutes(data);\n            } else {\n                updateRoutes(data);\n            }\n        }\n        if (sizeChanged) {\n            google.maps.event.trigger(map, \"resize\");\n            if (!dontFitMapBounds) {\n                var bounds = new google.maps.LatLngBounds();\n                for (var p in polylines) {\n                    extendBounds(bounds, polylines[p]);\n                }\n                fitMapBounds(bounds);\n            }\n        }\n        \n        for (var t in tooltips) {\n            var tooltip = tooltips[t];\n            var text = tooltip.pattern;\n            var replaceInfo = tooltip.replaceInfo;\n            for (var v in replaceInfo.variables) {\n                var variableInfo = replaceInfo.variables[v];\n                var txtVal = '''';\n                if (variableInfo.dataKeyIndex > -1) {\n                    var varData = data[variableInfo.dataKeyIndex].data;\n                    if (varData.length > 0) {\n                        var val = varData[varData.length-1][1];\n                        if (isNumber(val)) {\n                            txtVal = padValue(val, variableInfo.valDec, 0);\n                        } else {\n                            txtVal = val;\n                        }\n                    }\n                }\n                text = text.split(variableInfo.variable).join(txtVal);\n            }\n            tooltip.infowindow.setContent(text);\n        }\n        \n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Route Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"gmApiKey\": {\n        \"title\": \"Google Maps API Key\",\n        \"type\": \"string\"\n      },\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all routes\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"routesSettings\": {\n            \"title\": \"Routes settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Route settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },\n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"tooltipPattern\": {\n                    \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units''  )\",\n                    \"type\": \"string\",\n                    \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  },\n                  \"strokeWeight\": {\n                    \"title\": \"Stroke weight\",\n                    \"type\": \"number\",\n                    \"default\": 2\n                  },\n                  \"strokeOpacity\": {\n                    \"title\": \"Stroke opacity\",\n                    \"type\": \"number\",\n                    \"default\": 1.0\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n      \"gmApiKey\"\n    ]\n  },\n  \"form\": [\n    \"gmApiKey\",\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"routesSettings\",\n        \"items\": [\n            \"routesSettings[].latKeyName\",\n            \"routesSettings[].lngKeyName\",\n            \"routesSettings[].showLabel\",\n            \"routesSettings[].label\",\n            \"routesSettings[].tooltipPattern\",\n            {\n                \"key\": \"routesSettings[].color\",\n                \"type\": \"color\"\n            },\n            \"routesSettings[].strokeWeight\",\n            \"routesSettings[].strokeOpacity\"\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.3467277073670627,\"funcBody\":\"var lats = [37.7696499,\\n37.7699074,\\n37.7699536,\\n37.7697242,\\n37.7695189,\\n37.7696889,\\n37.7697153,\\n37.7701244,\\n37.7700604,\\n37.7705491,\\n37.7715705,\\n37.771752,\\n37.7707533,\\n37.769866];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lats[i];\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.058309787276281666,\"funcBody\":\"var lons = [-122.4261215,\\n-122.4219157,\\n-122.4199623,\\n-122.4179074,\\n-122.4155876,\\n-122.4155521,\\n-122.4163203,\\n-122.4193876,\\n-122.4210496,\\n-122.422284,\\n-122.4232717,\\n-122.4235138,\\n-122.4247605,\\n-122.4258812];\\n\\nvar i = Math.floor((time/3 % 14000) / 1000);\\n\\nreturn lons[i];\"}],\"intervalSec\":60}],\"timewindow\":{\"realtime\":{\"timewindowMs\":30000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"fitMapBounds\":false,\"routesSettings\":[{\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"color\":\"#1976d2\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"label\":\"First route\",\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"}],\"defaultZoomLevel\":16},\"title\":\"Route Map\"}"}',
+'Route Map' );
+
+INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
 VALUES ( now ( ), minTimeuuid ( 0 ), 'charts', 'basic_timeseries',
 '{"type":"timeseries","sizeX":8,"sizeY":6,"resources":[{"url":"https://rawgithub.com/HumbleSoftware/Flotr2/master/flotr2.min.js"}],"templateHtml":"","templateCss":"","controllerScript":"var graph, options;\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n\n    var colors = [];\n    for (var i in data) {\n        data[i].label = data[i].dataKey.label;\n        colors.push(data[i].dataKey.color);\n        var keySettings = data[i].dataKey.settings;\n\n        data[i].lines = {\n            fill: keySettings.fillLines || false,\n            show: keySettings.showLines || true\n        };\n\n        data[i].points = {\n            show: keySettings.showPoints || false\n        };\n    }\n    options = {\n        colors: colors,\n        title: null,\n        subtitle: null,\n        shadowSize: settings.shadowSize || 4,\n        fontColor: settings.fontColor || \"#545454\",\n        fontSize: settings.fontSize || 7.5,\n        xaxis: {\n            mode: ''time'',\n            timeMode: ''local''\n        },\n        yaxis: {\n        },\n        HtmlText: false,\n        grid: {\n            verticalLines: true,\n            horizontalLines: true\n        }\n    };\n    if (settings.grid) {\n        options.grid.color = settings.grid.color || \"#545454\";\n        options.grid.backgroundColor = settings.grid.backgroundColor || null;\n        options.grid.tickColor = settings.grid.tickColor || \"#DDDDDD\";\n        options.grid.verticalLines = settings.grid.verticalLines !== false;\n        options.grid.horizontalLines = settings.grid.horizontalLines !== false;\n    }\n    if (settings.xaxis) {\n        options.xaxis.showLabels = settings.xaxis.showLabels !== false;\n        options.xaxis.color = settings.xaxis.color || null;\n        options.xaxis.title = settings.xaxis.title || null;\n        options.xaxis.titleAngle = settings.xaxis.titleAngle || 0;\n    }\n    if (settings.yaxis) {\n        options.yaxis.showLabels = settings.yaxis.showLabels !== false;\n        options.yaxis.color = settings.yaxis.color || null;\n        options.yaxis.title = settings.yaxis.title || null;\n        options.yaxis.titleAngle = settings.yaxis.titleAngle || 0;\n    }\n}\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n    options.xaxis.min = timeWindow.minTime;\n    options.xaxis.max = timeWindow.maxTime;\n    graph = Flotr.draw(containerElement, data, options);\n};\n\nfns.destroy = function() {\n    //console.log(''destroy!'');\n};","settingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"Settings\",\n        \"properties\": {\n            \"shadowSize\": {\n                \"title\": \"Shadow size\",\n                \"type\": \"number\",\n                \"default\": 4\n            },\n            \"fontColor\": {\n                \"title\": \"Font color\",\n                \"type\": \"string\",\n                \"default\": \"#545454\"\n            },\n            \"fontSize\": {\n                \"title\": \"Font size\",\n                \"type\": \"number\",\n                \"default\": 7.5\n            },\n            \"grid\": {\n                \"title\": \"Grid settings\",\n                \"type\": \"object\",\n                \"properties\": {\n                    \"color\": {\n                        \"title\": \"Primary color\",\n                        \"type\": \"string\",\n                        \"default\": \"#545454\"\n                    },\n                    \"backgroundColor\": {\n                        \"title\": \"Background color\",\n                        \"type\": \"string\",\n                        \"default\": null\n                    },\n                    \"tickColor\": {\n                        \"title\": \"Ticks color\",\n                        \"type\": \"string\",\n                        \"default\": \"#DDDDDD\"\n                    },\n                    \"verticalLines\": {\n                        \"title\": \"Show vertical lines\",\n                        \"type\": \"boolean\",\n                        \"default\": true\n                    },\n                    \"horizontalLines\": {\n                        \"title\": \"Show horizontal lines\",\n                        \"type\": \"boolean\",\n                        \"default\": true\n                    }\n                }\n            },\n            \"xaxis\": {\n                \"title\": \"X axis settings\",\n                \"type\": \"object\",\n                \"properties\": {\n                    \"showLabels\": {\n                        \"title\": \"Show labels\",\n                        \"type\": \"boolean\",\n                        \"default\": true\n                    },\n                    \"title\": {\n                        \"title\": \"Axis title\",\n                        \"type\": \"string\",\n                        \"default\": null\n                    },\n                    \"titleAngle\": {\n                        \"title\": \"Axis title''s angle in degrees\",\n                        \"type\": \"number\",\n                        \"default\": 0\n                    },\n                    \"color\": {\n                        \"title\": \"Ticks color\",\n                        \"type\": \"string\",\n                        \"default\": null\n                    }\n                }\n            },\n            \"yaxis\": {\n                \"title\": \"Y axis settings\",\n                \"type\": \"object\",\n                \"properties\": {\n                    \"showLabels\": {\n                        \"title\": \"Show labels\",\n                        \"type\": \"boolean\",\n                        \"default\": true\n                    },\n                    \"title\": {\n                        \"title\": \"Axis title\",\n                        \"type\": \"string\",\n                        \"default\": null\n                    },\n                    \"titleAngle\": {\n                        \"title\": \"Axis title''s angle in degrees\",\n                        \"type\": \"number\",\n                        \"default\": 0\n                    },\n                    \"color\": {\n                        \"title\": \"Ticks color\",\n                        \"type\": \"string\",\n                        \"default\": null\n                    }\n                }\n            }\n        },\n        \"required\": []\n    },\n    \"form\": [\n        \"shadowSize\", \n        {\n            \"key\": \"fontColor\",\n            \"type\": \"color\"\n        },\n        \"fontSize\", \n        {\n            \"key\": \"grid\",\n            \"items\": [\n                {\n                    \"key\": \"grid.color\",\n                    \"type\": \"color\"\n                },\n                {\n                    \"key\": \"grid.backgroundColor\",\n                    \"type\": \"color\"\n                },\n                {\n                    \"key\": \"grid.tickColor\",\n                    \"type\": \"color\"\n                },\n                \"grid.verticalLines\",\n                \"grid.horizontalLines\"\n            ]\n        },\n        {\n            \"key\": \"xaxis\",\n            \"items\": [\n                \"xaxis.showLabels\",\n                \"xaxis.title\",\n                \"xaxis.titleAngle\",\n                {\n                    \"key\": \"xaxis.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        },\n        {\n            \"key\": \"yaxis\",\n            \"items\": [\n                \"yaxis.showLabels\",\n                \"yaxis.title\",\n                \"yaxis.titleAngle\",\n                {\n                    \"key\": \"yaxis.color\",\n                    \"type\": \"color\"\n                }\n            ]\n        }\n\n    ]\n}","dataKeySettingsSchema":"{\n    \"schema\": {\n        \"type\": \"object\",\n        \"title\": \"DataKeySettings\",\n        \"properties\": {\n            \"showLines\": {\n                \"title\": \"Show lines\",\n                \"type\": \"boolean\",\n                \"default\": true\n            },\n            \"fillLines\": {\n                \"title\": \"Fill lines\",\n                \"type\": \"boolean\",\n                \"default\": false\n            },\n            \"showPoints\": {\n                \"title\": \"Show points\",\n                \"type\": \"boolean\",\n                \"default\": false\n            }\n        },\n        \"required\": [\"showLines\", \"fillLines\", \"showPoints\"]\n    },\n    \"form\": [\n        \"showLines\",\n        \"fillLines\",\n        \"showPoints\"\n    ]\n}","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":7.5,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"backgroundColor\":\"#ffffff\"}},\"title\":\"Timeseries - Flotr2\"}"}',
 'Timeseries - Flotr2' );
@@ -193,8 +203,8 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', 'digital_vertical_bar',
 'Digital vertical bar' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
-VALUES ( now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
-'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".tb-marker-label {\n    border: none;\n    background: none;\n    box-shadow: none;\n}\n\n.tb-marker-label:before {\n    border: none;\n    background: none;\n}\n","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar markerCluster;\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    var configuredMarkersSettings = settings.markersSettings;\n    if (!configuredMarkersSettings) {\n        configuredMarkersSettings = [];\n    }\n    \n    for (var i=0;i<datasources.length;i++) {\n        markersSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,\n            color: \"FE7569\"\n        };\n        if (configuredMarkersSettings[i]) {\n            markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n            markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n            markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n            markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n            markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n        }\n    }\n    \n    map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n    L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n        attribution: ''&copy; <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n    }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n    \n    function createMarker(location, settings) {\n        var pinColor = settings.color;\n\n        var icon = L.icon({\n            iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n            iconSize: [21, 34],\n            iconAnchor: [10, 34],\n            popupAnchor: [0, -34],\n            shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n            shadowSize: [40, 37],\n            shadowAnchor: [12, 35]\n        });\n        \n        var marker = L.marker(location, {icon: icon}).addTo(map);\n        marker.bindPopup(''<b>'' + settings.label + ''</b>'');\n        if (settings.showLabel) {\n            marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n        }\n        return marker;\n    }\n    \n    function updatePosition(position, data) {\n        if (position.latIndex > -1 && position.lngIndex > -1) {\n            var latData = data[position.latIndex].data;\n            var lngData = data[position.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var lat = latData[latData.length-1][1];\n                var lng = lngData[lngData.length-1][1];\n                var location = L.latLng(lat, lng);\n                if (!position.marker) {\n                    position.marker = createMarker(location, position.settings);\n                    markers.push(position.marker);\n                    return true;\n                } else {\n                    var prevPosition = position.marker.getLatLng();\n                    if (!prevPosition.equals(location)) {\n                        position.marker.setLatLng(location);\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n    \n    function loadPositions(data) {\n        var bounds = L.latLngBounds();\n        positions = [];\n        var datasourceIndex = -1;\n        var markerSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                markerSettings = markersSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === markerSettings.latKeyName ||\n                dataKey.label === markerSettings.lngKeyName) {\n                var position = positions[datasourceIndex];\n                if (!position) {\n                    position = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: markerSettings\n                    };\n                    positions[datasourceIndex] = position;\n                } else if (position.marker) {\n                    continue;\n                }\n                if (dataKey.label === markerSettings.latKeyName) {\n                    position.latIndex = i;\n                } else {\n                    position.lngIndex = i;\n                }\n                if (position.latIndex > -1 && position.lngIndex > -1) {\n                    updatePosition(position, data);\n                    if (position.marker) {\n                        bounds.extend(position.marker.getLatLng());\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n    \n    function updatePositions(data) {\n        var positionsChanged = false;\n        var bounds = L.latLngBounds();\n        for (var p in positions) {\n            var position = positions[p];\n            positionsChanged |= updatePosition(position, data);\n            if (position.marker) {\n                bounds.extend(position.marker.getLatLng());\n            }\n        }\n        if (!dontFitMapBounds && positionsChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        map.once(''zoomend'', function(event) {\n            var zoomLevel = defaultZoomLevel || map.getZoom();\n            map.setZoom(zoomLevel, {animate: false});\n            if (!defaultZoomLevel && this.getZoom() > 15) {\n                map.setZoom(15, {animate: false});\n            }\n        });\n        map.fitBounds(bounds, {padding: [50, 50], animate: false});\n    }\n    \n    if (map) {\n        if (data) {\n            if (!positions) {\n                loadPositions(data);\n            } else {\n                updatePositions(data);\n            }\n        }\n        if (sizeChanged) {\n            map.invalidateSize(true);\n            var bounds = L.latLngBounds();\n            for (var m in markers) {\n                bounds.extend(markers[m].getLatLng());\n            }\n            fitMapBounds(bounds);\n        }\n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Google Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all markers\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"markersSettings\": {\n            \"title\": \"Markers settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Marker settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },                  \n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n    ]\n  },\n  \"form\": [\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"markersSettings\",\n        \"items\": [\n            \"markersSettings[].latKeyName\",\n            \"markersSettings[].lngKeyName\",\n            \"markersSettings[].showLabel\",\n            \"markersSettings[].label\",\n            {\n                \"key\": \"markersSettings[].color\",\n                \"type\": \"color\"\n            }\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}"}',
+VALUES (  now ( ), minTimeuuid ( 0 ), 'maps', 'openstreetmap',
+'{"type":"latest","sizeX":8.5,"sizeY":6,"resources":[{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.css"},{"url":"https://unpkg.com/leaflet@1.0.1/dist/leaflet.js"}],"templateHtml":"","templateCss":".tb-marker-label {\n    border: none;\n    background: none;\n    box-shadow: none;\n}\n\n.tb-marker-label:before {\n    border: none;\n    background: none;\n}\n","controllerScript":"var map;\nvar positions;\nvar markers = [];\nvar markersSettings = [];\nvar defaultZoomLevel;\nvar dontFitMapBounds;\n\nvar varsRegex = /\\$\\{([^\\}]*)\\}/g;\n\nvar tooltips = [];\n\nfns.init = function(containerElement, settings, datasources,\n    data) {\n    \n    if (settings.defaultZoomLevel) {\n        if (settings.defaultZoomLevel > 0 && settings.defaultZoomLevel < 21) {\n            defaultZoomLevel = Math.floor(settings.defaultZoomLevel);\n        }\n    }\n    \n    dontFitMapBounds = settings.fitMapBounds === false;\n    \n    function procesTooltipPattern(pattern, datasource, datasourceOffset) {\n        var match = varsRegex.exec(pattern);\n        var replaceInfo = {};\n        replaceInfo.variables = [];\n        while (match !== null) {\n            var variableInfo = {};\n            variableInfo.dataKeyIndex = -1;\n            var variable = match[0];\n            var label = match[1];\n            var valDec = 2;\n            var splitVals = label.split('':'');\n            if (splitVals.length > 1) {\n                label = splitVals[0];\n                valDec = parseFloat(splitVals[1]);\n            }\n            variableInfo.variable = variable;\n            variableInfo.valDec = valDec;\n            \n            if (label.startsWith(''#'')) {\n                var keyIndexStr = label.substring(1);\n                var n = Math.floor(Number(keyIndexStr));\n                if (String(n) === keyIndexStr && n >= 0) {\n                    variableInfo.dataKeyIndex = datasourceOffset + n;\n                }\n            }\n            if (variableInfo.dataKeyIndex === -1) {\n                for (var i = 0; i < datasource.dataKeys.length; i++) {\n                     var dataKey = datasource.dataKeys[i];\n                     if (dataKey.label === label) {\n                         variableInfo.dataKeyIndex = datasourceOffset + i;\n                         break;\n                     }\n                }\n            }\n            replaceInfo.variables.push(variableInfo);\n            match = varsRegex.exec(pattern);\n        }\n        return replaceInfo;\n    }    \n    \n    var configuredMarkersSettings = settings.markersSettings;\n    if (!configuredMarkersSettings) {\n        configuredMarkersSettings = [];\n    }\n    \n    var datasourceOffset = 0;\n    for (var i=0;i<datasources.length;i++) {\n        markersSettings[i] = {\n            latKeyName: \"lat\",\n            lngKeyName: \"lng\",\n            showLabel: true,\n            label: datasources[i].name,\n            color: \"FE7569\",\n            tooltipPattern: \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n        };\n        if (configuredMarkersSettings[i]) {\n            markersSettings[i].latKeyName = configuredMarkersSettings[i].latKeyName || markersSettings[i].latKeyName;\n            markersSettings[i].lngKeyName = configuredMarkersSettings[i].lngKeyName || markersSettings[i].lngKeyName;\n            \n            markersSettings[i].tooltipPattern = configuredMarkersSettings[i].tooltipPattern || \"<b>Latitude:</b> ${\"+markersSettings[i].latKeyName+\":7}<br/><b>Longitude:</b> ${\"+markersSettings[i].lngKeyName+\":7}\";\n            \n            markersSettings[i].tooltipReplaceInfo = procesTooltipPattern(markersSettings[i].tooltipPattern, datasources[i], datasourceOffset);            \n            \n            markersSettings[i].showLabel = configuredMarkersSettings[i].showLabel !== false;\n            markersSettings[i].label = configuredMarkersSettings[i].label || markersSettings[i].label;\n            markersSettings[i].color = configuredMarkersSettings[i].color ? tinycolor(configuredMarkersSettings[i].color).toHex() : markersSettings[i].color;\n        }\n        datasourceOffset += datasources[i].dataKeys.length;\n    }\n    \n    map = L.map(containerElement).setView([0, 0], defaultZoomLevel || 8);\n\n    L.tileLayer(''http://{s}.tile.osm.org/{z}/{x}/{y}.png'', {\n        attribution: ''&copy; <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors''\n    }).addTo(map);\n\n\n}\n\n\nfns.redraw = function(containerElement, width, height, data,\n    timeWindow, sizeChanged) {\n    \n    function isNumber(n) {\n        return !isNaN(parseFloat(n)) && isFinite(n);\n    }\n    \n    function padValue(val, dec, int) {\n        var i = 0;\n        var s, strVal, n;\n    \n        val = parseFloat(val);\n        n = (val < 0);\n        val = Math.abs(val);\n    \n        if (dec > 0) {\n            strVal = val.toFixed(dec).toString().split(''.'');\n            s = int - strVal[0].length;\n    \n            for (; i < s; ++i) {\n                strVal[0] = ''0'' + strVal[0];\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal[0] + ''.'' + strVal[1];\n        }\n    \n        else {\n            strVal = Math.round(val).toString();\n            s = int - strVal.length;\n    \n            for (; i < s; ++i) {\n                strVal = ''0'' + strVal;\n            }\n    \n            strVal = (n ? ''-'' : '''') + strVal;\n        }\n    \n        return strVal;\n    }                \n    \n    function createMarker(location, settings) {\n        var pinColor = settings.color;\n\n        var icon = L.icon({\n            iconUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|'' + pinColor,\n            iconSize: [21, 34],\n            iconAnchor: [10, 34],\n            popupAnchor: [0, -34],\n            shadowUrl: ''http://chart.apis.google.com/chart?chst=d_map_pin_shadow'',\n            shadowSize: [40, 37],\n            shadowAnchor: [12, 35]\n        });\n        \n        var marker = L.marker(location, {icon: icon}).addTo(map);\n        if (settings.showLabel) {\n            marker.bindTooltip(''<b>'' + settings.label + ''</b>'', { className: ''tb-marker-label'', permanent: true, direction: ''top'', offset: [0, -24] });\n        }\n        \n        createTooltip(marker, settings.tooltipPattern, settings.tooltipReplaceInfo);\n        \n        return marker;\n    }\n    \n        \n    function createTooltip(marker, pattern, replaceInfo) {\n        var popup = L.popup();\n        popup.setContent('''');\n        marker.bindPopup(popup);\n        tooltips.push( {\n            popup: popup,\n            pattern: pattern,\n            replaceInfo: replaceInfo\n        });\n    }\n    \n    function updatePosition(position, data) {\n        if (position.latIndex > -1 && position.lngIndex > -1) {\n            var latData = data[position.latIndex].data;\n            var lngData = data[position.lngIndex].data;\n            if (latData.length > 0 && lngData.length > 0) {\n                var lat = latData[latData.length-1][1];\n                var lng = lngData[lngData.length-1][1];\n                var location = L.latLng(lat, lng);\n                if (!position.marker) {\n                    position.marker = createMarker(location, position.settings);\n                    markers.push(position.marker);\n                    return true;\n                } else {\n                    var prevPosition = position.marker.getLatLng();\n                    if (!prevPosition.equals(location)) {\n                        position.marker.setLatLng(location);\n                        return true;\n                    }\n                }\n            }\n        }\n        return false;\n    }\n    \n    function loadPositions(data) {\n        var bounds = L.latLngBounds();\n        positions = [];\n        var datasourceIndex = -1;\n        var markerSettings;\n        var datasource;\n        for (var i = 0; i < data.length; i++) {\n            var datasourceData = data[i];\n            if (!datasource || datasource != datasourceData.datasource) {\n                datasourceIndex++;\n                datasource = datasourceData.datasource;\n                markerSettings = markersSettings[datasourceIndex];\n            }\n            var dataKey = datasourceData.dataKey;\n            if (dataKey.label === markerSettings.latKeyName ||\n                dataKey.label === markerSettings.lngKeyName) {\n                var position = positions[datasourceIndex];\n                if (!position) {\n                    position = {\n                        latIndex: -1,\n                        lngIndex: -1,\n                        settings: markerSettings\n                    };\n                    positions[datasourceIndex] = position;\n                } else if (position.marker) {\n                    continue;\n                }\n                if (dataKey.label === markerSettings.latKeyName) {\n                    position.latIndex = i;\n                } else {\n                    position.lngIndex = i;\n                }\n                if (position.latIndex > -1 && position.lngIndex > -1) {\n                    updatePosition(position, data);\n                    if (position.marker) {\n                        bounds.extend(position.marker.getLatLng());\n                    }\n                }\n            }\n        }\n        fitMapBounds(bounds);\n    }\n    \n    function updatePositions(data) {\n        var positionsChanged = false;\n        var bounds = L.latLngBounds();\n        for (var p in positions) {\n            var position = positions[p];\n            positionsChanged |= updatePosition(position, data);\n            if (position.marker) {\n                bounds.extend(position.marker.getLatLng());\n            }\n        }\n        if (!dontFitMapBounds && positionsChanged) {\n            fitMapBounds(bounds);\n        }\n    }\n    \n    function fitMapBounds(bounds) {\n        map.once(''zoomend'', function(event) {\n            var zoomLevel = defaultZoomLevel || map.getZoom();\n            map.setZoom(zoomLevel, {animate: false});\n            if (!defaultZoomLevel && this.getZoom() > 15) {\n                map.setZoom(15, {animate: false});\n            }\n        });\n        map.fitBounds(bounds, {padding: [50, 50], animate: false});\n    }\n    \n    if (map) {\n        if (data) {\n            if (!positions) {\n                loadPositions(data);\n            } else {\n                updatePositions(data);\n            }\n        }\n        if (sizeChanged) {\n            map.invalidateSize(true);\n            var bounds = L.latLngBounds();\n            for (var m in markers) {\n                bounds.extend(markers[m].getLatLng());\n            }\n            fitMapBounds(bounds);\n        }\n        \n        for (var t in tooltips) {\n            var tooltip = tooltips[t];\n            var text = tooltip.pattern;\n            var replaceInfo = tooltip.replaceInfo;\n            for (var v in replaceInfo.variables) {\n                var variableInfo = replaceInfo.variables[v];\n                var txtVal = '''';\n                if (variableInfo.dataKeyIndex > -1) {\n                    var varData = data[variableInfo.dataKeyIndex].data;\n                    if (varData.length > 0) {\n                        var val = varData[varData.length-1][1];\n                        if (isNumber(val)) {\n                            txtVal = padValue(val, variableInfo.valDec, 0);\n                        } else {\n                            txtVal = val;\n                        }\n                    }\n                }\n                text = text.split(variableInfo.variable).join(txtVal);\n            }\n            tooltip.popup.setContent(text);\n        }    \n        \n    }\n\n};","settingsSchema":"{\n  \"schema\": {\n    \"title\": \"Google Map Configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n      \"defaultZoomLevel\": {\n         \"title\": \"Default map zoom level (1 - 20)\",\n         \"type\": \"number\"\n      },\n      \"fitMapBounds\": {\n          \"title\": \"Fit map bounds to cover all markers\",\n          \"type\": \"boolean\",\n          \"default\": true\n      },\n      \"markersSettings\": {\n            \"title\": \"Markers settings, same order as datasources\",\n            \"type\": \"array\",\n            \"items\": {\n              \"title\": \"Marker settings\",\n              \"type\": \"object\",\n              \"properties\": {\n                  \"latKeyName\": {\n                    \"title\": \"Latitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lat\"\n                  },\n                  \"lngKeyName\": {\n                    \"title\": \"Longitude key name\",\n                    \"type\": \"string\",\n                    \"default\": \"lng\"\n                  },                  \n                  \"showLabel\": {\n                    \"title\": \"Show label\",\n                    \"type\": \"boolean\",\n                    \"default\": true\n                  },                  \n                  \"label\": {\n                    \"title\": \"Label\",\n                    \"type\": \"string\"\n                  },\n                  \"tooltipPattern\": {\n                    \"title\": \"Pattern ( for ex. ''Text ${keyName} units.'' or ''${#<key index>} units''  )\",\n                    \"type\": \"string\",\n                    \"default\": \"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"\n                  },\n                  \"color\": {\n                    \"title\": \"Color\",\n                    \"type\": \"string\"\n                  }\n              }\n            }\n      }\n    },\n    \"required\": [\n    ]\n  },\n  \"form\": [\n    \"defaultZoomLevel\",\n    \"fitMapBounds\",\n    {\n        \"key\": \"markersSettings\",\n        \"items\": [\n            \"markersSettings[].latKeyName\",\n            \"markersSettings[].lngKeyName\",\n            \"markersSettings[].showLabel\",\n            \"markersSettings[].label\",\n            \"markersSettings[].tooltipPattern\",\n            {\n                \"key\": \"markersSettings[].color\",\n                \"type\": \"color\"\n            }\n        ]\n    }\n  ]\n}","dataKeySettingsSchema":"{}\n","defaultConfig":"{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.05427416942713381,\"funcBody\":\"var value = prevValue || 15.833293;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.680594833308841,\"funcBody\":\"var value = prevValue || -90.454350;\\nif (time % 5000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]},{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lat\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.05012157428742059,\"funcBody\":\"var value = prevValue || 14.450463;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"lng\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.6742359401617628,\"funcBody\":\"var value = prevValue || -84.845334;\\nif (time % 4000 < 500) {\\n    value += Math.random() * 0.05 - 0.025;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"markersSettings\":[{\"label\":\"First point\",\"color\":\"#1e88e5\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${latitude:7}<br/><b>Longitude:</b> ${longitude:7}\"},{\"label\":\"Second point\",\"color\":\"#fdd835\",\"latKeyName\":\"lat\",\"lngKeyName\":\"lng\",\"showLabel\":true,\"tooltipPattern\":\"<b>Latitude:</b> ${lat:7}<br/><b>Longitude:</b> ${lng:7}\"}],\"fitMapBounds\":true},\"title\":\"OpenStreetMap\"}"}',
 'OpenStreetMap' );
 
 INSERT INTO "thingsboard"."widget_type" ( "id", "tenant_id", "bundle_alias", "alias", "descriptor", "name" )
diff --git a/dao/src/test/resources/logback.xml b/dao/src/test/resources/logback.xml
index 0969bbe..046d8f5 100644
--- a/dao/src/test/resources/logback.xml
+++ b/dao/src/test/resources/logback.xml
@@ -7,7 +7,7 @@
         </encoder>
     </appender>
 
-    <logger name="org.thingsboard.server.dao" level="TRACE"/>
+    <logger name="org.thingsboard.server.dao" level="WARN"/>
     <logger name="org.apache.cassandra" level="WARN"/>
     <logger name="org.cassandraunit" level="INFO" />
     <logger name="org.apache.cassandra" level="INFO" />
diff --git a/docker/deploy_cassandra_zookeeper.sh b/docker/deploy_cassandra_zookeeper.sh
index 263ef49..d2fd9c8 100755
--- a/docker/deploy_cassandra_zookeeper.sh
+++ b/docker/deploy_cassandra_zookeeper.sh
@@ -28,4 +28,4 @@ echo "building images.."
 $command build
 
 echo "starting cassandra, zookeeper, thingsboard-db-schema images..."
-$command up -d cassandra zookeeper thingsboard-db-schema
+$command up -d db zk thingsboard-db-schema
diff --git a/docker/docker-compose.random.yml b/docker/docker-compose.random.yml
index 9b51901..d70107a 100644
--- a/docker/docker-compose.random.yml
+++ b/docker/docker-compose.random.yml
@@ -17,10 +17,10 @@
 version: '2'
 
 services:
-  cassandra:
+  db:
     ports:
       - "9042"
       - "9160"
-  zookeeper:
+  zk:
     ports:
       - "2181"
diff --git a/docker/docker-compose.static.yml b/docker/docker-compose.static.yml
index bdaf4eb..1661378 100644
--- a/docker/docker-compose.static.yml
+++ b/docker/docker-compose.static.yml
@@ -17,10 +17,10 @@
 version: '2'
 
 services:
-  cassandra:
+  db:
     ports:
       - "9042:9042"
       - "9160:9160"
-  zookeeper:
+  zk:
     ports:
       - "2181:2181"
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index b15b7fa..185d54f 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -23,24 +23,18 @@ services:
       - "8080:8080"
       - "1883:1883"
       - "5683:5683"
-    links:
-      - cassandra:db
-      - zookeeper:zk
-      - thingsboard-db-schema:thingsboard-db-schema
     env_file:
       - thingsboard.env
     entrypoint: ./run_thingsboard.sh
   thingsboard-db-schema:
     image: "thingsboard/thingsboard-db-schema:1.0"
-    links:
-      - cassandra:db
     env_file:
       - thingsboard-db-schema.env
     entrypoint: ./install_schema.sh
-  cassandra:
+  db:
     image: "cassandra:3.9"
     volumes:
       - "${CASSANDRA_DATA_DIR}:/var/lib/cassandra"
-  zookeeper:
+  zk:
     image: "zookeeper:3.4.9"
     restart: always
diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index 6070d0b..431482b 100644
--- a/extensions/extension-kafka/pom.xml
+++ b/extensions/extension-kafka/pom.xml
@@ -22,7 +22,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java b/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java
index 1642fb5..7321ad7 100644
--- a/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java
+++ b/extensions/extension-kafka/src/main/java/org/thingsboard/server/extensions/kafka/plugin/KafkaPlugin.java
@@ -47,7 +47,7 @@ public class KafkaPlugin extends AbstractPlugin<KafkaPluginConfiguration> {
         properties.put("buffer.memory", configuration.getBufferMemory());
         if (configuration.getOtherProperties() != null) {
             configuration.getOtherProperties()
-                    .stream().forEach(p -> properties.put(p.getKey(), p.getValue()));
+                    .forEach(p -> properties.put(p.getKey(), p.getValue()));
         }
         init();
     }
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index c764f60..99167bd 100644
--- a/extensions/extension-rabbitmq/pom.xml
+++ b/extensions/extension-rabbitmq/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml
index 59428e2..39072ee 100644
--- a/extensions/extension-rest-api-call/pom.xml
+++ b/extensions/extension-rest-api-call/pom.xml
@@ -22,7 +22,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/pom.xml b/extensions/pom.xml
index e825d1b..fc48d41 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index ed4dd2d..aad2469 100644
--- a/extensions-api/pom.xml
+++ b/extensions-api/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java
new file mode 100644
index 0000000..0104824
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceCredentialsUpdateNotificationMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 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.api.device;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+
+import java.util.Set;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class DeviceCredentialsUpdateNotificationMsg implements ToDeviceActorNotificationMsg {
+
+    private final TenantId tenantId;
+    private final DeviceId deviceId;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
index fab11bb..1633c7f 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
@@ -91,7 +91,7 @@ public class DefaultWebsocketMsgHandler implements WebsocketMsgHandler {
     }
 
     public void clear(PluginContext ctx) {
-        wsSessionsMap.values().stream().forEach(v -> {
+        wsSessionsMap.values().forEach(v -> {
             try {
                 ctx.close(v.getSessionRef());
             } catch (IOException e) {
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index d8581e6..f4e7697 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
index 21180d7..7123f3e 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
@@ -40,7 +40,9 @@ public class MethodNameFilter extends SimpleRuleLifecycleComponent implements Ru
 
     @Override
     public void init(MethodNameFilterConfiguration configuration) {
-        methods = Arrays.asList(configuration.getMethodNames()).stream().map(m -> m.getName()).collect(Collectors.toSet());
+        methods = Arrays.stream(configuration.getMethodNames())
+                .map(m -> m.getName())
+                .collect(Collectors.toSet());
     }
 
     @Override
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
index 84deea5..737bee6 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
@@ -39,7 +39,7 @@ public class MsgTypeFilter extends SimpleRuleLifecycleComponent implements RuleF
 
     @Override
     public void init(MsgTypeFilterConfiguration configuration) {
-        msgTypes = Arrays.asList(configuration.getMessageTypes()).stream().map(type -> {
+        msgTypes = Arrays.stream(configuration.getMessageTypes()).map(type -> {
             switch (type) {
                 case "GET_ATTRIBUTES":
                     return MsgType.GET_ATTRIBUTES_REQUEST;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
index 52fd2e9..c8a7ad8 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
@@ -75,7 +75,7 @@ public class MailPlugin extends AbstractPlugin<MailPluginConfiguration> implemen
         if (configuration.getOtherProperties() != null) {
             Properties mailProperties = new Properties();
             configuration.getOtherProperties()
-                    .stream().forEach(p -> mailProperties.put(p.getKey(), p.getValue()));
+                    .forEach(p -> mailProperties.put(p.getKey(), p.getValue()));
             mail.setJavaMailProperties(mailProperties);
         }
         mailSender = mail;
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 b166dae..06467fe 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
@@ -97,7 +97,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         builder.setDeviceId(cmd.getDeviceId().toString());
         builder.setType(cmd.getType().name());
         builder.setAllKeys(cmd.isAllKeys());
-        cmd.getKeyStates().entrySet().stream().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
+        cmd.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
         ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLAZZ, builder.build().toByteArray()));
     }
 
@@ -144,7 +144,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         if (update.getErrorMsg() != null) {
             builder.setErrorMsg(update.getErrorMsg());
         }
-        update.getData().entrySet().stream().forEach(
+        update.getData().entrySet().forEach(
                 e -> {
                     SubscriptionUpdateValueListProto.Builder dataBuilder = SubscriptionUpdateValueListProto.newBuilder();
 
@@ -166,7 +166,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
             return new SubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
         } else {
             Map<String, List<Object>> data = new TreeMap<>();
-            proto.getDataList().stream().forEach(v -> {
+            proto.getDataList().forEach(v -> {
                 List<Object> values = data.get(v.getKey());
                 if (values == null) {
                     values = new ArrayList<>();
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 6ea7489..8e2d62a 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
@@ -109,8 +109,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                     sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
 
                     Map<String, Long> subState = new HashMap<>(keys.size());
-                    keys.stream().forEach(key -> subState.put(key, 0L));
-                    attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                    keys.forEach(key -> subState.put(key, 0L));
+                    attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
 
                     sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
                 } else {
@@ -119,7 +119,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                     sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
 
                     Map<String, Long> subState = new HashMap<>(attributesData.size());
-                    attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                    attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
 
                     sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
                 }
@@ -154,8 +154,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                         sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
 
                         Map<String, Long> subState = new HashMap<>(keys.size());
-                        keys.stream().forEach(key -> subState.put(key, startTs));
-                        data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                        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);
                     } else {
@@ -168,8 +168,8 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                                 sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
 
                                 Map<String, Long> subState = new HashMap<>(keys.size());
-                                keys.stream().forEach(key -> subState.put(key, startTs));
-                                data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                                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);
                             }
@@ -188,7 +188,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                         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.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                            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);
                         }
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 637500e..190d9ff 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
@@ -68,7 +68,7 @@ public class SubscriptionManager {
         registerSubscription(sessionId, deviceId, subscription);
         List<TsKvEntry> missedUpdates = new ArrayList<>();
         if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
-            subscription.getKeyStates().entrySet().stream().forEach(e -> {
+            subscription.getKeyStates().entrySet().forEach(e -> {
                         Optional<AttributeKvEntry> latestOpt = ctx.loadAttribute(deviceId, DataConstants.CLIENT_SCOPE, e.getKey());
                         if (latestOpt.isPresent()) {
                             AttributeKvEntry latestEntry = latestOpt.get();
diff --git a/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
index 3095f5d..bcaeeb5 100644
--- a/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
+++ b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
@@ -71,7 +71,7 @@ public class DeviceAttributesFilterTest {
         filter.stop();
     }
 
-    @Test(timeout = 10000)
+    @Test(timeout = 30000)
     public void basicClientAttributesStressTest() {
         DeviceAttributesFilter filter = new DeviceAttributesFilter();
         filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 59bc9e2..8e9ac9f 100755
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.thingsboard</groupId>
     <artifactId>thingsboard</artifactId>
-    <version>1.0.0</version>
+    <version>1.0.1</version>
     <packaging>pom</packaging>
 
     <name>Thingsboard</name>

README.md 4(+3 -1)

diff --git a/README.md b/README.md
index 4b08ffd..e308800 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# Thingsboard
+# Thingsboard 
+[![Join the chat at https://gitter.im/thingsboard/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/thingsboard/chat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Build Status](https://travis-ci.org/thingsboard/thingsboard.svg?branch=master)](https://travis-ci.org/thingsboard/thingsboard)
 
 Thingsboard is an open-source IoT platform for data collection, processing, visualization, and device management.
 

tools/pom.xml 41(+39 -2)

diff --git a/tools/pom.xml b/tools/pom.xml
index 4134fc5..4ef32d4 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
@@ -78,6 +78,43 @@
             <artifactId>mockito-all</artifactId>
             <scope>test</scope>
         </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/MqttStressTestClient.java b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestClient.java
index b0ddf73..5805cad 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestClient.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestClient.java
@@ -40,10 +40,10 @@ public class MqttStressTestClient {
         this.client = new MqttAsyncClient(brokerUri, clientId, persistence);
     }
 
-    public void connect() throws MqttException {
+    public IMqttToken connect() throws MqttException {
         MqttConnectOptions options = new MqttConnectOptions();
         options.setUserName(deviceToken);
-        client.connect(options, null, new IMqttActionListener() {
+        return client.connect(options, null, new IMqttActionListener() {
             @Override
             public void onSuccess(IMqttToken iMqttToken) {
                 log.info("OnSuccess");
@@ -60,6 +60,22 @@ public class MqttStressTestClient {
         client.disconnect();
     }
 
+
+
+    public void warmUp(byte[] data) throws MqttException {
+        MqttMessage msg = new MqttMessage(data);
+        client.publish("v1/devices/me/telemetry", msg, null, new IMqttActionListener() {
+            @Override
+            public void onSuccess(IMqttToken asyncActionToken) {
+            }
+
+            @Override
+            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+            }
+        }).waitForCompletion();
+    }
+
+
     public void publishTelemetry(byte[] data) throws MqttException {
         long sendTime = System.currentTimeMillis();
         MqttMessage msg = new MqttMessage(data);
@@ -67,14 +83,12 @@ public class MqttStressTestClient {
             @Override
             public void onSuccess(IMqttToken asyncActionToken) {
                 long ackTime = System.currentTimeMillis();
-//                log.info("Delivery time: {}", ackTime - sendTime);
                 results.onResult(true, ackTime - sendTime);
             }
 
             @Override
             public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                 long failTime = System.currentTimeMillis();
-//                log.info("Failure time: {}", failTime - sendTime);
                 results.onResult(false, failTime - sendTime);
             }
         });
diff --git a/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java
index 900d81f..ed6f42b 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/MqttStressTestTool.java
@@ -1,4 +1,4 @@
-package org.thingsboard.client.tools; /**
+/**
  * Copyright © 2016 The Thingsboard Authors
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,14 +13,32 @@ package org.thingsboard.client.tools; /**
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.thingsboard.client.tools; /**
+ * Copyright © 2016 The Thingsboard Authors
+ * <p>
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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 lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
+import java.util.UUID;
+import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
@@ -29,60 +47,83 @@ import java.util.concurrent.atomic.AtomicLong;
 @Slf4j
 public class MqttStressTestTool {
 
-    private static final long TEST_DURATION = TimeUnit.MINUTES.toMillis(1);
-    private static final long TEST_ITERATION = TimeUnit.MILLISECONDS.toMillis(100);
-    private static final long TEST_SUB_ITERATION = TimeUnit.MILLISECONDS.toMillis(2);
-    private static final int DEVICE_COUNT = 100;
-    private static final String BASE_URL = "http://localhost:8080";
-    private static final String[] MQTT_URLS = {"tcp://localhost:1883"};
-//    private static final String[] MQTT_URLS = {"tcp://localhost:1883", "tcp://localhost:1884", "tcp://localhost:1885"};
-    private static final String USERNAME = "tenant@thingsboard.org";
-    private static final String PASSWORD = "tenant";
+    public static void main(String[] args) throws Exception {
+        TestParams params = new TestParams();
+        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
 
 
-    public static void main(String[] args) throws Exception {
+        if (params.getDuration() % params.getIterationInterval() != 0) {
+            throw new IllegalArgumentException("Test Duration % Iteration Interval != 0");
+        }
+
+        if ((params.getIterationInterval() * 1000) % params.getDeviceCount() != 0) {
+            throw new IllegalArgumentException("Iteration Interval % Device Count != 0");
+        }
+
         ResultAccumulator results = new ResultAccumulator();
 
         AtomicLong value = new AtomicLong(Long.MAX_VALUE);
         log.info("value: {} ", value.incrementAndGet());
 
-        RestClient restClient = new RestClient(BASE_URL);
-        restClient.login(USERNAME, PASSWORD);
+        RestClient restClient = new RestClient(params.getRestApiUrl());
+        restClient.login(params.getUsername(), params.getPassword());
 
         List<MqttStressTestClient> clients = new ArrayList<>();
-        for (int i = 0; i < DEVICE_COUNT; i++) {
-            Device device = restClient.createDevice("Device " + i);
+        List<IMqttToken> connectTokens = new ArrayList<>();
+        for (int i = 0; i < params.getDeviceCount(); i++) {
+            Device device = restClient.createDevice("Device " + UUID.randomUUID());
             DeviceCredentials credentials = restClient.getCredentials(device.getId());
-            String mqttURL = MQTT_URLS[i % MQTT_URLS.length];
+            String[] mqttUrls = params.getMqttUrls();
+            String mqttURL = mqttUrls[i % mqttUrls.length];
             MqttStressTestClient client = new MqttStressTestClient(results, mqttURL, credentials.getCredentialsId());
-            client.connect();
+            connectTokens.add(client.connect());
             clients.add(client);
         }
-        Thread.sleep(1000);
 
+        for (IMqttToken tokens : connectTokens) {
+            tokens.waitForCompletion();
+        }
 
         byte[] data = "{\"longKey\":73}".getBytes(StandardCharsets.UTF_8);
-        long startTime = System.currentTimeMillis();
-        int iterationsCount = (int) (TEST_DURATION / TEST_ITERATION);
-        int subIterationsCount = (int) (TEST_ITERATION / TEST_SUB_ITERATION);
-        if (clients.size() % subIterationsCount != 0) {
-            throw new IllegalArgumentException("Invalid parameter exception!");
+
+        for (MqttStressTestClient client : clients) {
+            client.warmUp(data);
         }
+
+        Thread.sleep(1000);
+
+        long startTime = System.currentTimeMillis();
+        int iterationsCount = (int) (params.getDuration() / params.getIterationInterval());
+        int subIterationMicroSeconds = (int) ((params.getIterationInterval() * 1000) / params.getDeviceCount());
+
+        List<ScheduledFuture<Void>> iterationFutures = new ArrayList<>();
         for (int i = 0; i < iterationsCount; i++) {
-            for (int j = 0; j < subIterationsCount; j++) {
-                int packSize = clients.size() / subIterationsCount;
-                for (int k = 0; k < packSize; k++) {
-                    int clientIndex = packSize * j + k;
-                    clients.get(clientIndex).publishTelemetry(data);
+            long delay = i * params.getIterationInterval();
+            iterationFutures.add(scheduler.schedule((Callable<Void>) () -> {
+                long sleepMicroSeconds = 0L;
+                for (MqttStressTestClient client : clients) {
+                    client.publishTelemetry(data);
+                    sleepMicroSeconds += subIterationMicroSeconds;
+                    if (sleepMicroSeconds > 1000) {
+                        Thread.sleep(sleepMicroSeconds / 1000);
+                        sleepMicroSeconds = sleepMicroSeconds % 1000;
+                    }
                 }
-                Thread.sleep(TEST_SUB_ITERATION);
-            }
+                return null;
+            }, delay, TimeUnit.MILLISECONDS));
         }
+
+        for (ScheduledFuture<Void> future : iterationFutures) {
+            future.get();
+        }
+
         Thread.sleep(1000);
+
         for (MqttStressTestClient client : clients) {
             client.disconnect();
         }
         log.info("Results: {} took {}ms", results, System.currentTimeMillis() - startTime);
+        scheduler.shutdownNow();
     }
 
 }
diff --git a/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java b/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java
index 5bba82a..1364fc1 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/ResultAccumulator.java
@@ -73,7 +73,7 @@ public class ResultAccumulator {
 
     @Override
     public String toString() {
-        return "org.thingsboard.client.tools.ResultAccumulator{" +
+        return "Result {" +
                 "successCount=" + getSuccessCount() +
                 ", errorCount=" + getErrorCount() +
                 ", totalTime=" + getTimeSpent() +
diff --git a/tools/src/main/java/org/thingsboard/client/tools/TestParams.java b/tools/src/main/java/org/thingsboard/client/tools/TestParams.java
new file mode 100644
index 0000000..eb1328b
--- /dev/null
+++ b/tools/src/main/java/org/thingsboard/client/tools/TestParams.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 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.client.tools;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class TestParams {
+    static final String TEST_PROPERTIES = "test.properties";
+    static final long DEFAULT_TEST_DURATION = TimeUnit.MINUTES.toMillis(1);
+    static final long DEFAULT_TEST_INTERVAL = TimeUnit.MILLISECONDS.toMillis(100);
+    static final int DEFAULT_DEVICE_COUNT = 100;
+    static final String DEFAULT_REST_URL = "http://localhost:8080";
+    static final String DEFAULT_MQTT_URLS = "tcp://localhost:1883";
+    static final String DEFAULT_USERNAME = "tenant@thingsboard.org";
+    static final String DEFAULT_PASSWORD = "tenant";
+
+    private Properties params = new Properties();
+
+    public TestParams() throws IOException {
+        try {
+            params.load(new FileInputStream(TEST_PROPERTIES));
+        } catch (Exception e) {
+            log.warn("Failed to read " + TEST_PROPERTIES);
+        }
+    }
+
+    public long getDuration() {
+        return Long.valueOf(params.getProperty("durationMs", Long.toString(DEFAULT_TEST_DURATION)));
+    }
+
+    public long getIterationInterval() {
+        return Long.valueOf(params.getProperty("iterationIntervalMs", Long.toString(DEFAULT_TEST_INTERVAL)));
+    }
+
+    public int getDeviceCount() {
+        return Integer.valueOf(params.getProperty("deviceCount", Integer.toString(DEFAULT_DEVICE_COUNT)));
+    }
+
+    public String getRestApiUrl() {
+        return params.getProperty("restUrl", DEFAULT_REST_URL);
+    }
+
+    public String[] getMqttUrls() {
+        return params.getProperty("mqttUrls", DEFAULT_MQTT_URLS).split(",");
+    }
+
+    public String getUsername() {
+        return params.getProperty("username", DEFAULT_USERNAME);
+    }
+
+    public String getPassword() {
+        return params.getProperty("password", DEFAULT_PASSWORD);
+    }
+}
diff --git a/tools/src/main/resources/logback.xml b/tools/src/main/resources/logback.xml
new file mode 100644
index 0000000..11973fa
--- /dev/null
+++ b/tools/src/main/resources/logback.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+    Copyright © 2016 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.
+
+-->
+<!DOCTYPE configuration>
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.thingsboard" level="INFO" />
+
+    <root level="INFO">
+        <appender-ref ref="STDOUT"/>
+    </root>
+
+</configuration>
\ No newline at end of file
diff --git a/tools/src/main/resources/test.properties b/tools/src/main/resources/test.properties
new file mode 100644
index 0000000..6e9ed89
--- /dev/null
+++ b/tools/src/main/resources/test.properties
@@ -0,0 +1,5 @@
+restUrl=http://localhost:8080
+mqttUrls=tcp://localhost:1883
+deviceCount=1
+durationMs=60000
+iterationIntervalMs=1000
diff --git a/tools/test.properties b/tools/test.properties
new file mode 100644
index 0000000..93efc60
--- /dev/null
+++ b/tools/test.properties
@@ -0,0 +1,3 @@
+deviceCount=1000
+durationMs=5000
+iterationIntervalMs=250
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index 6221596..09fb573 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
index a61f0bf..6f6e35a 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
@@ -36,6 +36,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.concurrent.atomic.AtomicInteger;
+
 @Slf4j
 public class CoapSessionCtx extends DeviceAwareSessionContext {
 
@@ -87,6 +88,8 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
     private void onSessionClose(SessionCloseMsg msg) {
         if (msg.isTimeout()) {
             exchange.respond(ResponseCode.SERVICE_UNAVAILABLE);
+        } else if (msg.isCredentialsRevoked()) {
+            exchange.respond(ResponseCode.UNAUTHORIZED);
         } else {
             exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
         }
@@ -120,7 +123,7 @@ public class CoapSessionCtx extends DeviceAwareSessionContext {
 
     public void close() {
         log.info("[{}] Closing processing context. Timeout: {}", sessionId, exchange.advanced().isTimedOut());
-        processor.process(new SessionCloseMsg(sessionId, exchange.advanced().isTimedOut()));
+        processor.process(exchange.advanced().isTimedOut() ? SessionCloseMsg.onTimeout(sessionId) : SessionCloseMsg.onError(sessionId));
     }
 
     @Override
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 4a5fc78..d1b68a5 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 9d332be..d5826b0 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
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 e1bb45c..b4d8108 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
@@ -210,7 +210,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
     }
 
     private void processDisconnect(ChannelHandlerContext ctx) {
-        processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
         ctx.close();
     }
 
@@ -255,6 +254,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
 
     @Override
     public void operationComplete(Future<? super Void> future) throws Exception {
-        processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
+        processor.process(SessionCloseMsg.onError(sessionCtx.getSessionId()));
     }
 }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
index 5cae9f5..f653682 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
@@ -16,12 +16,13 @@
 package org.thingsboard.server.transport.mqtt.session;
 
 import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.mqtt.MqttMessage;
+import io.netty.handler.codec.mqtt.*;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.common.data.id.SessionId;
 import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
 import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
 import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
 import org.thingsboard.server.common.msg.session.ex.SessionException;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
@@ -75,7 +76,10 @@ public class MqttSessionCtx extends DeviceAwareSessionContext {
 
     @Override
     public void onMsg(SessionCtrlMsg msg) throws SessionException {
-
+        if (msg instanceof SessionCloseMsg) {
+            pushToNetwork(new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)));
+            channel.close();
+        }
     }
 
     @Override
diff --git a/transport/pom.xml b/transport/pom.xml
index a03dd83..ac12461 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>

ui/.eslintrc 3(+3 -0)

diff --git a/ui/.eslintrc b/ui/.eslintrc
index 5cb89e5..5b8920d 100644
--- a/ui/.eslintrc
+++ b/ui/.eslintrc
@@ -11,5 +11,8 @@
       "node_modules",
       "\\.tpl\\.html$"
     ]
+  },
+  "globals": {
+    "FileReader": true
   }
 }

ui/package.json 20(+11 -9)

diff --git a/ui/package.json b/ui/package.json
index d8978a7..c1101d2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
 {
   "name": "thingsboard",
   "private": true,
-  "version": "1.0.0",
+  "version": "1.0.1",
   "description": "Thingsboard UI",
   "licenses": [
     {
@@ -14,13 +14,14 @@
     "build": "NODE_ENV=production webpack -p"
   },
   "dependencies": {
+    "@flowjs/ng-flow": "^2.7.1",
     "ace-builds": "^1.2.5",
-    "angular": "^1.5.8",
-    "angular-animate": "^1.5.8",
-    "angular-aria": "^1.5.8",
+    "angular": "1.5.8",
+    "angular-animate": "1.5.8",
+    "angular-aria": "1.5.8",
     "angular-breadcrumb": "^0.4.1",
     "angular-carousel": "^1.0.1",
-    "angular-cookies": "^1.5.8",
+    "angular-cookies": "1.5.8",
     "angular-drag-and-drop-lists": "^1.4.0",
     "angular-fullscreen": "git://github.com/fabiobiondi/angular-fullscreen.git#master",
     "angular-gridster": "^0.13.14",
@@ -29,11 +30,11 @@
     "angular-material": "^1.1.1",
     "angular-material-data-table": "^0.10.9",
     "angular-material-icons": "^0.7.1",
-    "angular-messages": "^1.5.8",
-    "angular-route": "^1.5.8",
-    "angular-sanitize": "^1.5.8",
+    "angular-messages": "1.5.8",
+    "angular-route": "1.5.8",
+    "angular-sanitize": "1.5.8",
     "angular-storage": "0.0.15",
-    "angular-touch": "^1.5.8",
+    "angular-touch": "1.5.8",
     "angular-translate": "^2.12.1",
     "angular-translate-handler-log": "^2.12.1",
     "angular-translate-interpolation-messageformat": "^2.12.1",
@@ -65,6 +66,7 @@
     "react": "^15.4.1",
     "react-ace": "^4.1.0",
     "react-dom": "^15.4.1",
+    "react-dropzone": "^3.7.3",
     "react-schema-form": "^0.3.1",
     "react-tap-event-plugin": "^2.0.1",
     "reactcss": "^1.0.9",

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

diff --git a/ui/pom.xml b/ui/pom.xml
index bc68c72..1559ecb 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -20,7 +20,7 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.thingsboard</groupId>
-        <version>1.0.0</version>
+        <version>1.0.1</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 3d55ac1..3aa6cea 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -42,6 +42,7 @@ import 'react-dom';
 import 'material-ui';
 import 'react-schema-form';
 import react from 'ngreact';
+import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
 
 import thingsboardLogin from './login';
 import thingsboardDialogs from './components/datakey-config-dialog.controller';
@@ -88,6 +89,7 @@ angular.module('thingsboard', [
     'angular-carousel',
     'ngclipboard',
     react.name,
+    'flow',
     thingsboardLogin,
     thingsboardDialogs,
     thingsboardMenu,
diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js
index 934f021..3f2e7d7 100644
--- a/ui/src/app/app.run.js
+++ b/ui/src/app/app.run.js
@@ -14,9 +14,12 @@
  * limitations under the License.
  */
 
+import Flow from '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+
 /*@ngInject*/
 export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $filter, loginService, userService, $translate) {
 
+    $window.Flow = Flow;
     var frame = $window.frameElement;
     var unauthorizedDialog = null;
     var forbiddenDialog = null;
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index ce7668f..e5a1b02 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -51,6 +51,7 @@ function Dashboard() {
             widgets: '=',
             deviceAliasList: '=',
             columns: '=',
+            margins: '=',
             isEdit: '=',
             isMobile: '=',
             isMobileDisabled: '=?',
@@ -61,7 +62,8 @@ function Dashboard() {
             onWidgetClicked: '&?',
             loadWidgets: '&?',
             onInit: '&?',
-            onInitFailed: '&?'
+            onInitFailed: '&?',
+            dashboardStyle: '=?'
         },
         controller: DashboardController,
         controllerAs: 'vm',
@@ -108,7 +110,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
         },
         isMobile: vm.isMobileDisabled ? false : vm.isMobile,
         mobileBreakPoint: vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960),
-        margins: [10, 10],
+        margins: vm.margins ? vm.margins : [10, 10],
         saveGridItemCalculatedHeightInMobile: true
     };
 
@@ -161,6 +163,10 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
         vm.gridsterOpts.columns = vm.columns ? vm.columns : 24;
     });
 
+    $scope.$watch('vm.margins', function () {
+        vm.gridsterOpts.margins = vm.margins ? vm.margins : [10, 10];
+    });
+
     $scope.$watch('vm.isEdit', function () {
         vm.gridsterOpts.resizable.enabled = vm.isEdit;
         vm.gridsterOpts.draggable.enabled = vm.isEdit;
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 1127cb0..f0cecef 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -20,61 +20,63 @@
 	<md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular>
 </md-content>
 <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap>
-	<div id="gridster-child" gridster="vm.gridsterOpts">
-		<ul>
-<!-- 			    			ng-click="widgetClicked($event, widget)"  --> 
-			<li gridster-item="widget" ng-repeat="widget in vm.widgets">
-			    <div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
-			    			ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
-					 		tb-mousedown="vm.widgetMouseDown($event, widget)"
-					 		tb-mousemove="vm.widgetMouseMove($event, widget)"
-			    			tb-mouseup="vm.widgetMouseUp($event, widget)"
-			        style="
-			        cursor: pointer;
-			        color: {{vm.widgetColor(widget)}};
-			        background-color: {{vm.widgetBackgroundColor(widget)}};
-			        padding: {{vm.widgetPadding(widget)}}
-			        ">
-			    	<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
-			    		<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
-				    	<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
-			    	</div>
-					<div class="tb-widget-actions" layout="row" layout-align="start center">
-						<md-button id="expand-button"
-								   aria-label="{{ 'fullscreen.fullscreen' | translate }}"
-								   class="md-icon-button md-primary"></md-button>
-						<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
-								   ng-disabled="vm.loading()"
-								   class="md-icon-button md-primary"
-								   ng-click="vm.editWidget($event, widget)"
-								   aria-label="{{ 'widget.edit' | translate }}">
-							<md-tooltip md-direction="top">
-								{{ 'widget.edit' | translate }}
-							</md-tooltip>
-							<md-icon class="material-icons">
-								edit
-							</md-icon>
-						</md-button>
-						<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
-				            ng-disabled="vm.loading()"
-				            class="md-icon-button md-primary"
-				            ng-click="vm.removeWidget($event, widget)"
-				            aria-label="{{ 'widget.remove' | translate }}">
-			                <md-tooltip md-direction="top">
-          						{{ 'widget.remove' | translate }}
-        					</md-tooltip>
-					        <md-icon class="material-icons">
-					          close
-					        </md-icon>		        					
-				        </md-button>	 						
-					</div>		    		
-			    	<div flex layout="column" class="tb-widget-content">
-			        	<div flex tb-widget
-			        		 locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+	<div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;">
+		<div id="gridster-child" gridster="vm.gridsterOpts">
+			<ul>
+	<!-- 			    			ng-click="widgetClicked($event, widget)"  -->
+				<li gridster-item="widget" ng-repeat="widget in vm.widgets">
+					<div tb-expand-fullscreen expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp"
+								ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}"
+								tb-mousedown="vm.widgetMouseDown($event, widget)"
+								tb-mousemove="vm.widgetMouseMove($event, widget)"
+								tb-mouseup="vm.widgetMouseUp($event, widget)"
+						style="
+						cursor: pointer;
+						color: {{vm.widgetColor(widget)}};
+						background-color: {{vm.widgetBackgroundColor(widget)}};
+						padding: {{vm.widgetPadding(widget)}}
+						">
+						<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
+							<span ng-show="vm.showWidgetTitle(widget)" class="md-subhead">{{widget.config.title}}</span>
+							<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
 						</div>
-			    	</div>
-			    </div>
-			</li>
-		</ul>
+						<div class="tb-widget-actions" layout="row" layout-align="start center">
+							<md-button id="expand-button"
+									   aria-label="{{ 'fullscreen.fullscreen' | translate }}"
+									   class="md-icon-button md-primary"></md-button>
+							<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
+									   ng-disabled="vm.loading()"
+									   class="md-icon-button md-primary"
+									   ng-click="vm.editWidget($event, widget)"
+									   aria-label="{{ 'widget.edit' | translate }}">
+								<md-tooltip md-direction="top">
+									{{ 'widget.edit' | translate }}
+								</md-tooltip>
+								<md-icon class="material-icons">
+									edit
+								</md-icon>
+							</md-button>
+							<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
+								ng-disabled="vm.loading()"
+								class="md-icon-button md-primary"
+								ng-click="vm.removeWidget($event, widget)"
+								aria-label="{{ 'widget.remove' | translate }}">
+								<md-tooltip md-direction="top">
+									{{ 'widget.remove' | translate }}
+								</md-tooltip>
+								<md-icon class="material-icons">
+								  close
+								</md-icon>
+							</md-button>
+						</div>
+						<div flex layout="column" class="tb-widget-content">
+							<div flex tb-widget
+								 locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
+							</div>
+						</div>
+					</div>
+				</li>
+			</ul>
+		</div>
 	</div>
 </md-content>
\ No newline at end of file
diff --git a/ui/src/app/components/react/json-form-image.jsx b/ui/src/app/components/react/json-form-image.jsx
new file mode 100644
index 0000000..2da3edc
--- /dev/null
+++ b/ui/src/app/components/react/json-form-image.jsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright © 2016 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 './json-form-image.scss';
+
+import React from 'react';
+import ThingsboardBaseComponent from './json-form-base-component.jsx';
+import Dropzone from 'react-dropzone';
+import IconButton from 'material-ui/IconButton';
+
+class ThingsboardImage extends React.Component {
+
+    constructor(props) {
+        super(props);
+        this.onValueChanged = this.onValueChanged.bind(this);
+        this.onDrop = this.onDrop.bind(this);
+        this.onClear = this.onClear.bind(this);
+        var value = props.value ? props.value + '' : null;
+        this.state = {
+            imageUrl: value
+        };
+    }
+
+    onValueChanged(value) {
+        this.setState({
+            imageUrl: value
+        });
+        this.props.onChangeValidate({
+            target: {
+                value: value
+            }
+        });
+    }
+
+    onDrop(files) {
+        var reader = new FileReader();
+        reader.onload = (function(tImg) {
+            return function(event) {
+                tImg.onValueChanged(event.target.result);
+            };
+        })(this);
+        reader.readAsDataURL(files[0]);
+    }
+
+    onClear(event) {
+        if (event) {
+            event.stopPropagation();
+        }
+        this.onValueChanged(null);
+    }
+
+    render() {
+
+        var labelClass = "tb-label";
+        if (this.props.form.required) {
+            labelClass += " tb-required";
+        }
+        if (this.props.form.readonly) {
+            labelClass += " tb-readonly";
+        }
+        if (this.state.focused) {
+            labelClass += " tb-focused";
+        }
+
+        var previewComponent;
+        if (this.state.imageUrl) {
+            previewComponent = <img className="tb-image-preview" src={this.state.imageUrl} />;
+        } else {
+            previewComponent = <div>No image selected</div>;
+        }
+
+        return (
+            <div className="tb-container">
+                <label className={labelClass}>{this.props.form.title}</label>
+                <div className="tb-image-select-container">
+                    <div className="tb-image-preview-container">{previewComponent}</div>
+                    <div className="tb-image-clear-container">
+                        <IconButton className="tb-image-clear-btn" iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
+                    </div>
+                    <Dropzone className="tb-dropzone"
+                              onDrop={this.onDrop}
+                              multiple={false}
+                              accept="image/*">
+                        <div>Drop an image or click to select a file to upload.</div>
+                    </Dropzone>
+                </div>
+            </div>
+        );
+    }
+}
+
+export default ThingsboardBaseComponent(ThingsboardImage);
\ No newline at end of file
diff --git a/ui/src/app/components/react/json-form-image.scss b/ui/src/app/components/react/json-form-image.scss
new file mode 100644
index 0000000..58b5eaa
--- /dev/null
+++ b/ui/src/app/components/react/json-form-image.scss
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 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.
+ */
+
+$previewSize: 100px;
+
+.tb-image-select-container {
+  position: relative;
+  height: $previewSize;
+  width: 100%;
+}
+
+.tb-image-preview {
+   max-width: $previewSize;
+   max-height: $previewSize;
+   width: 100%;
+   height: 100%;
+}
+
+.tb-image-preview-container {
+   position: relative;
+   width: $previewSize;
+   height: $previewSize;
+   margin-right: 12px;
+   border: solid 1px;
+   vertical-align: top;
+   float: left;
+   div {
+      width: 100%;
+      font-size: 18px;
+      text-align: center;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%,-50%);
+   }
+}
+
+.tb-dropzone {
+   position: relative;
+   border: dashed 2px;
+   height: $previewSize;
+   vertical-align: top;
+   padding: 0 8px;
+   overflow: hidden;
+   div {
+      width: 100%;
+      font-size: 24px;
+      text-align: center;
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%,-50%);
+   }
+}
+
+.tb-image-clear-container {
+   width: 48px;
+   height: $previewSize;
+   position: relative;
+   float: right;
+}
+.tb-image-clear-btn {
+   position: absolute !important;
+   top: 50%;
+   transform: translate(0%,-50%) !important;
+}
diff --git a/ui/src/app/components/react/json-form-schema-form.jsx b/ui/src/app/components/react/json-form-schema-form.jsx
index 3e1c046..c641a0a 100644
--- a/ui/src/app/components/react/json-form-schema-form.jsx
+++ b/ui/src/app/components/react/json-form-schema-form.jsx
@@ -26,6 +26,7 @@ import ThingsboardText from './json-form-text.jsx';
 import Select from 'react-schema-form/lib/Select';
 import Radios from 'react-schema-form/lib/Radios';
 import ThingsboardDate from './json-form-date.jsx';
+import ThingsboardImage from './json-form-image.jsx';
 import ThingsboardCheckbox from './json-form-checkbox.jsx';
 import Help from 'react-schema-form/lib/Help';
 import ThingsboardFieldSet from './json-form-fieldset.jsx';
@@ -45,6 +46,7 @@ class ThingsboardSchemaForm extends React.Component {
             'select': Select,
             'radios': Radios,
             'date': ThingsboardDate,
+            'image': ThingsboardImage,
             'checkbox': ThingsboardCheckbox,
             'help': Help,
             'array': ThingsboardArray,
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 18c2b5f..68978c3 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -16,6 +16,7 @@
 /* eslint-disable import/no-unresolved, import/default */
 
 import deviceAliasesTemplate from './device-aliases.tpl.html';
+import dashboardBackgroundTemplate from './dashboard-settings.tpl.html';
 import addWidgetTemplate from './add-widget.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
@@ -55,6 +56,7 @@ export default function DashboardController(types, widgetService, userService,
     vm.onAddWidgetClosed = onAddWidgetClosed;
     vm.onEditWidgetClosed = onEditWidgetClosed;
     vm.openDeviceAliases = openDeviceAliases;
+    vm.openDashboardSettings = openDashboardSettings;
     vm.removeWidget = removeWidget;
     vm.saveDashboard = saveDashboard;
     vm.saveWidget = saveWidget;
@@ -252,6 +254,24 @@ export default function DashboardController(types, widgetService, userService,
         });
     }
 
+    function openDashboardSettings($event) {
+        $mdDialog.show({
+            controller: 'DashboardSettingsController',
+            controllerAs: 'vm',
+            templateUrl: dashboardBackgroundTemplate,
+            locals: {
+                gridSettings: angular.copy(vm.dashboard.configuration.gridSettings)
+            },
+            parent: angular.element($document[0].body),
+            skipHide: true,
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (gridSettings) {
+            vm.dashboard.configuration.gridSettings = gridSettings;
+        }, function () {
+        });
+    }
+
     function editWidget($event, widget) {
         $event.stopPropagation();
         var newEditingIndex = vm.widgets.indexOf(widget);
@@ -368,6 +388,15 @@ export default function DashboardController(types, widgetService, userService,
                         w.triggerHandler('resize');
                     }
                 }).then(function (widget) {
+                    var columns = 24;
+                    if (vm.dashboard.configuration.gridSettings && vm.dashboard.configuration.gridSettings.columns) {
+                        columns = vm.dashboard.configuration.gridSettings.columns;
+                    }
+                    if (columns != 24) {
+                        var ratio = columns / 24;
+                        widget.sizeX *= ratio;
+                        widget.sizeY *= ratio;
+                    }
                     vm.widgets.push(widget);
                 }, function () {
                 });
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index b18c392..0c815a6 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -59,10 +59,22 @@
         <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDeviceAliases($event)">
             {{ 'device.aliases' | translate }}
         </md-button>
+        <md-button class="md-raised" flex="none" ng-show="vm.isEdit" ng-click="vm.openDashboardSettings($event)">
+            {{ 'dashboard.settings' | translate }}
+        </md-button>
     </section>
-    <div class="tb-absolute-fill" ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
+    <div class="tb-absolute-fill"
+         ng-class="{ 'tb-padded' : !vm.widgetEditMode, 'tb-shrinked' : vm.isEditingWidget }">
         <tb-dashboard
+                dashboard-style="{'background-color': vm.dashboard.configuration.gridSettings.backgroundColor,
+                                  'background-image': 'url('+vm.dashboard.configuration.gridSettings.backgroundImageUrl+')',
+                                  'background-repeat': 'no-repeat',
+                                  'background-attachment': 'scroll',
+                                  'background-size': '100%',
+                                  'background-position': '0% 0%'}"
                 widgets="vm.widgets"
+                columns="vm.dashboard.configuration.gridSettings.columns"
+                margins="vm.dashboard.configuration.gridSettings.margins"
                 device-alias-list="vm.dashboard.configuration.deviceAliases"
                 is-edit="vm.isEdit || vm.widgetEditMode"
                 is-mobile="vm.forceDashboardMobileMode"
diff --git a/ui/src/app/dashboard/dashboard-settings.controller.js b/ui/src/app/dashboard/dashboard-settings.controller.js
new file mode 100644
index 0000000..d15359e
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.controller.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './dashboard-settings.scss';
+
+/*@ngInject*/
+export default function DashboardSettingsController($scope, $mdDialog, gridSettings) {
+
+    var vm = this;
+
+    vm.cancel = cancel;
+    vm.save = save;
+    vm.imageAdded = imageAdded;
+    vm.clearImage = clearImage;
+
+    vm.gridSettings = gridSettings || {};
+
+    vm.gridSettings.backgroundColor = vm.gridSettings.backgroundColor || 'rgba(0,0,0,0)';
+    vm.gridSettings.columns = vm.gridSettings.columns || 24;
+    vm.gridSettings.margins = vm.gridSettings.margins || [10, 10];
+    vm.hMargin = vm.gridSettings.margins[0];
+    vm.vMargin = vm.gridSettings.margins[1];
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function imageAdded($file) {
+        var reader = new FileReader();
+        reader.onload = function(event) {
+            $scope.$apply(function() {
+                if (event.target.result && event.target.result.startsWith('data:image/')) {
+                    $scope.theForm.$setDirty();
+                    vm.gridSettings.backgroundImageUrl = event.target.result;
+                }
+            });
+        };
+        reader.readAsDataURL($file.file);
+    }
+
+    function clearImage() {
+        $scope.theForm.$setDirty();
+        vm.gridSettings.backgroundImageUrl = null;
+    }
+
+    function save() {
+        $scope.theForm.$setPristine();
+        vm.gridSettings.margins = [vm.hMargin, vm.vMargin];
+        $mdDialog.hide(vm.gridSettings);
+    }
+}
diff --git a/ui/src/app/dashboard/dashboard-settings.scss b/ui/src/app/dashboard/dashboard-settings.scss
new file mode 100644
index 0000000..2231372
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.scss
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016 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.
+ */
+$previewSize: 100px;
+
+.file-input {
+  display: none;
+}
+
+.tb-container {
+  position: relative;
+  margin-top: 32px;
+  padding: 10px 0;
+}
+
+.tb-image-select-container {
+  position: relative;
+  height: $previewSize;
+  width: 100%;
+}
+
+.tb-image-preview {
+  max-width: $previewSize;
+  max-height: $previewSize;
+  width: auto;
+  height: auto;
+}
+
+.tb-image-preview-container {
+  position: relative;
+  width: $previewSize;
+  height: $previewSize;
+  margin-right: 12px;
+  border: solid 1px;
+  vertical-align: top;
+  float: left;
+  div {
+    width: 100%;
+    font-size: 18px;
+    text-align: center;
+  }
+  div, .tb-image-preview {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%,-50%);
+  }
+}
+
+.tb-flow-drop {
+  position: relative;
+  border: dashed 2px;
+  height: $previewSize;
+  vertical-align: top;
+  padding: 0 8px;
+  overflow: hidden;
+  min-width: 300px;
+  label {
+    width: 100%;
+    font-size: 24px;
+    text-align: center;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%,-50%);
+  }
+}
+
+.tb-image-clear-container {
+  width: 48px;
+  height: $previewSize;
+  position: relative;
+  float: right;
+}
+.tb-image-clear-btn {
+  position: absolute !important;
+  top: 50%;
+  transform: translate(0%,-50%) !important;
+}
diff --git a/ui/src/app/dashboard/dashboard-settings.tpl.html b/ui/src/app/dashboard/dashboard-settings.tpl.html
new file mode 100644
index 0000000..f69eb02
--- /dev/null
+++ b/ui/src/app/dashboard/dashboard-settings.tpl.html
@@ -0,0 +1,115 @@
+<!--
+
+    Copyright © 2016 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.settings' | translate }}">
+    <form name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>dashboard.settings</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-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">
+                    <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"
+                               max="1000" />
+                        <div ng-messages="theForm.columns.$error" multiple md-auto-hide="false">
+                            <div ng-message="required" translate>dashboard.columns-count-required</div>
+                            <div ng-message="min" translate>dashboard.min-columns-count-message</div>
+                            <div ng-message="max">dashboard.max-columns-count-message</div>
+                        </div>
+                    </md-input-container>
+                    <small translate>dashboard.widgets-margins</small>
+                    <div flex layout="row">
+                        <md-input-container flex class="md-block">
+                            <label translate>dashboard.horizontal-margin</label>
+                            <input required type="number" step="any" name="hMargin" ng-model="vm.hMargin" min="0"
+                                   max="50" />
+                            <div ng-messages="theForm.hMargin.$error" multiple md-auto-hide="false">
+                                <div ng-message="required" translate>dashboard.horizontal-margin-required</div>
+                                <div ng-message="min" translate>dashboard.min-horizontal-margin-message</div>
+                                <div ng-message="max" translate>dashboard.max-horizontal-margin-message</div>
+                            </div>
+                        </md-input-container>
+                        <md-input-container flex class="md-block">
+                            <label translate>dashboard.vertical-margin</label>
+                            <input required type="number" step="any" name="vMargin" ng-model="vm.vMargin" min="0"
+                                   max="50" />
+                            <div ng-messages="theForm.vMargin.$error" multiple md-auto-hide="false">
+                                <div ng-message="required" translate>dashboard.vertical-margin-required</div>
+                                <div ng-message="min" translate>dashboard.min-vertical-margin-message</div>
+                                <div ng-message="max" translate>dashboard.max-vertical-margin-message</div>
+                            </div>
+                        </md-input-container>
+                    </div>
+                    <div flex
+                         ng-required="false"
+                         md-color-picker
+                         ng-model="vm.gridSettings.backgroundColor"
+                         label="{{ 'dashboard.background-color' | translate }}"
+                         icon="format_color_fill"
+                         default="rgba(0,0,0,0)"
+                         md-color-clear-button="false"
+                         open-on-input="true"
+                         md-color-generic-palette="false"
+                         md-color-history="false"
+                    ></div>
+                    <div class="tb-container">
+                        <label class="tb-label" translate>dashboard.background-image</label>
+                        <div flow-init="{singleFile:true}"
+                             flow-file-added="vm.imageAdded( $file )" class="tb-image-select-container">
+                            <div class="tb-image-preview-container">
+                                <div ng-show="!vm.gridSettings.backgroundImageUrl" translate>dashboard.no-image</div>
+                                <img ng-show="vm.gridSettings.backgroundImageUrl" class="tb-image-preview" src="{{vm.gridSettings.backgroundImageUrl}}" />
+                            </div>
+                            <div class="tb-image-clear-container">
+                                <md-button ng-click="vm.clearImage()"
+                                           class="tb-image-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+                                    <md-tooltip md-direction="top">
+                                        {{ 'action.remove' | translate }}
+                                    </md-tooltip>
+                                    <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+                                        close
+                                    </md-icon>
+                                </md-button>
+                            </div>
+                            <div class="alert tb-flow-drop" flow-drop>
+                                <label for="select" translate>dashboard.drop-image</label>
+                                <input class="file-input" flow-btn flow-attrs="{accept:'image/*'}" id="select">
+                            </div>
+                        </div>
+                    </div>
+                </fieldset>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid" 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>
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index d740b10..7400463 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -34,6 +34,7 @@ import DashboardRoutes from './dashboard.routes';
 import DashboardsController from './dashboards.controller';
 import DashboardController from './dashboard.controller';
 import DeviceAliasesController from './device-aliases.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';
@@ -59,6 +60,7 @@ export default angular.module('thingsboard.dashboard', [
     .controller('DashboardsController', DashboardsController)
     .controller('DashboardController', DashboardController)
     .controller('DeviceAliasesController', DeviceAliasesController)
+    .controller('DashboardSettingsController', DashboardSettingsController)
     .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController)
     .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController)
     .controller('AddWidgetController', AddWidgetController)
diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js
index 918040b..7f84016 100644
--- a/ui/src/app/device/device.directive.js
+++ b/ui/src/app/device/device.directive.js
@@ -20,18 +20,23 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export default function DeviceDirective($compile, $templateCache, toast, $translate, types, customerService) {
+export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) {
     var linker = function (scope, element) {
         var template = $templateCache.get(deviceFieldsetTemplate);
         element.html(template);
 
         scope.isAssignedToCustomer = false;
-
         scope.assignedCustomer = null;
 
+        scope.deviceCredentials = null;
 
         scope.$watch('device', function(newVal) {
             if (newVal) {
+                deviceService.getDeviceCredentials(scope.device.id.id).then(
+                    function success(credentials) {
+                        scope.deviceCredentials = credentials;
+                    }
+                );
                 if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) {
                     scope.isAssignedToCustomer = true;
                     customerService.getCustomer(scope.device.customerId.id).then(
@@ -50,6 +55,10 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
             toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
         };
 
+        scope.onAccessTokenCopied = function() {
+            toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
         $compile(element.contents())(scope);
     }
     return {
diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html
index 6d9892e..3bd8f4f 100644
--- a/ui/src/app/device/device-fieldset.tpl.html
+++ b/ui/src/app/device/device-fieldset.tpl.html
@@ -36,6 +36,13 @@
         <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
         <span translate>device.copyId</span>
     </md-button>
+    <md-button ngclipboard data-clipboard-action="copy"
+               ngclipboard-success="onAccessTokenCopied(e)"
+               data-clipboard-text="{{deviceCredentials.credentialsId}}" ng-show="!isEdit"
+               class="md-raised">
+        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+        <span translate>device.copyAccessToken</span>
+    </md-button>
 </div>
 
 <md-content class="md-padding" layout="column">
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 4a5e48d..9da68d6 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -192,7 +192,26 @@
     "select-existing": "Select existing dashboard",
     "create-new": "Create new dashboard",
     "new-dashboard-title": "New dashboard title",
-    "open-dashboard": "Open dashboard"
+    "open-dashboard": "Open dashboard",
+    "set-background": "Set background",
+    "background-color": "Background color",
+    "background-image": "Background image",
+    "no-image": "No image selected",
+    "drop-image": "Drop an image or click to select a file to upload.",
+    "settings": "Settings",
+    "columns-count": "Columns count",
+    "columns-count-required": "Columns count is required.",
+    "min-columns-count-message": "Only 10 minimum column count is allowed.",
+    "max-columns-count-message": "Only 1000 maximum column count is allowed.",
+    "widgets-margins": "Margin between widgets",
+    "horizontal-margin": "Horizontal margin",
+    "horizontal-margin-required": "Horizontal margin value is required.",
+    "min-horizontal-margin-message": "Only 0 is allowed as minimum horizontal margin value.",
+    "max-horizontal-margin-message": "Only 50 is allowed as maximum horizontal margin value.",
+    "vertical-margin": "Vertical margin",
+    "vertical-margin-required": "Vertical margin value is required.",
+    "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
+    "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value."
   },
   "datakey": {
     "settings": "Settings",
@@ -280,7 +299,9 @@
     "events": "Events",
     "details": "Details",
     "copyId": "Copy device Id",
+    "copyAccessToken": "Copy access token",
     "idCopiedMessage": "Device Id has been copied to clipboard",
+    "accessTokenCopiedMessage": "Device access token has been copied to clipboard",
     "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}}"