thingsboard-memoizeit

Changes

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

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

pom.xml 2(+1 -1)

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

ui/.eslintrc 3(+3 -0)

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

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

Details

diff --git a/application/pom.xml b/application/pom.xml
index 0c53121..a5559ee 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
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/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/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 90b0cb3..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,6 +118,7 @@ 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().forEach(v -> {
                     forwardToAppActor(context, v, currentTargetServer);
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/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/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index aa7fcc1..2b42bdd 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -138,8 +138,8 @@ cassandra:
     default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
     # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
     ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
-    # Specify max partitions per request
-    max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:1000}"
+    # Specify max data points per request
+    max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:86400}"
 
 # Actor system parameters
 actors:
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java
new file mode 100644
index 0000000..b1be92a
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/MqttServerSideRpcIntegrationTest.java
@@ -0,0 +1,108 @@
+/**
+ * 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.mqtt.rpc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.*;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.client.tools.RestClient;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.mqtt.AbstractFeatureIntegrationTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public class MqttServerSideRpcIntegrationTest extends AbstractFeatureIntegrationTest {
+
+    private static final String MQTT_URL = "tcp://localhost:1883";
+    private static final String BASE_URL = "http://localhost:8080";
+
+    private static final String USERNAME = "tenant@thingsboard.org";
+    private static final String PASSWORD = "tenant";
+
+    private Device savedDevice;
+
+    private String accessToken;
+    private RestClient restClient;
+
+    @Before
+    public void beforeTest() throws Exception {
+        restClient = new RestClient(BASE_URL);
+        restClient.login(USERNAME, PASSWORD);
+
+        Device device = new Device();
+        device.setName("Test Server-Side RPC Device");
+        savedDevice = restClient.getRestTemplate().postForEntity(BASE_URL + "/api/device", device, Device.class).getBody();
+        DeviceCredentials deviceCredentials =
+                restClient.getRestTemplate().getForEntity(BASE_URL + "/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class).getBody();
+        assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+        accessToken = deviceCredentials.getCredentialsId();
+        assertNotNull(accessToken);
+    }
+
+    @Test
+    public void testServerMqttTwoWayRpc() throws Exception {
+        String clientId = MqttAsyncClient.generateClientId();
+        MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
+
+        MqttConnectOptions options = new MqttConnectOptions();
+        options.setUserName(accessToken);
+        client.connect(options);
+        Thread.sleep(3000);
+        client.subscribe("v1/devices/me/rpc/request/+",1);
+        client.setCallback(new TestMqttCallback(client));
+
+        String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
+        String deviceId = savedDevice.getId().getId().toString();
+        String result = restClient.getRestTemplate().postForEntity(BASE_URL + "api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class).getBody();
+        log.info("Result: " + result);
+        Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result);
+    }
+
+    private static class TestMqttCallback implements MqttCallback {
+
+        private final MqttAsyncClient client;
+
+        TestMqttCallback(MqttAsyncClient client) {
+            this.client = client;
+        }
+
+        @Override
+        public void connectionLost(Throwable throwable) {
+        }
+
+        @Override
+        public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
+            log.info("Message Arrived: " + mqttMessage.getPayload().toString());
+            MqttMessage message = new MqttMessage();
+            String responseTopic = requestTopic.replace("request", "response");
+            message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes());
+            client.publish(responseTopic, message);
+        }
+
+        @Override
+        public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
+
+        }
+    }
+}
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 343ada2..971f570 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>common</artifactId>
     </parent>
     <groupId>org.thingsboard.common</groupId>
diff --git a/common/message/pom.xml b/common/message/pom.xml
index c62732e..13ac70f 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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 58ea1cb..4494af7 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 75f8e00..c49c6eb 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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 d1e84df..a2edc7a 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/dao/src/main/resources/demo-data.cql b/dao/src/main/resources/demo-data.cql
index 84a1dff..cc62b05 100644
--- a/dao/src/main/resources/demo-data.cql
+++ b/dao/src/main/resources/demo-data.cql
@@ -287,10 +287,10 @@ VALUES (
 	'org.thingsboard.server.extensions.core.plugin.mail.MailPlugin',
 	true,
 	'{
-       "host": "smtp.gmail.com",
-       "port": 587,
-       "username": "username@gmail.com",
-       "password": "password",
+       "host": "smtp.sendgrid.net",
+       "port": 2525,
+       "username": "apikey",
+       "password": "your_api_key",
        "otherProperties": [
          {
            "key":"mail.smtp.auth",
@@ -303,14 +303,6 @@ VALUES (
          {
            "key":"mail.smtp.starttls.enable",
            "value":"true"
-         },
-         {
-           "key":"mail.smtp.host",
-           "value":"smtp.gmail.com"
-         },
-         {
-           "key":"mail.smtp.port",
-           "value":"587"
          }
        ]
      }'
diff --git a/dao/src/main/resources/system-data.cql b/dao/src/main/resources/system-data.cql
index 1358e58..f91243a 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' );
@@ -194,7 +204,7 @@ VALUES ( now ( ), minTimeuuid ( 0 ), 'digital_gauges', '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\"}"}',
+'{"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":".leaflet-zoom-box {\n\tz-index: 9;\n}\n\n.leaflet-pane         { z-index: 4; }\n\n.leaflet-tile-pane    { z-index: 2; }\n.leaflet-overlay-pane { z-index: 4; }\n.leaflet-shadow-pane  { z-index: 5; }\n.leaflet-marker-pane  { z-index: 6; }\n.leaflet-tooltip-pane   { z-index: 7; }\n.leaflet-popup-pane   { z-index: 8; }\n\n.leaflet-map-pane canvas { z-index: 1; }\n.leaflet-map-pane svg    { z-index: 2; }\n\n.leaflet-control {\n\tz-index: 9;\n}\n.leaflet-top,\n.leaflet-bottom {\n\tz-index: 11;\n}\n\n.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, {autoClose: false, closeOnClick: false});\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/docker/docker-compose.yml b/docker/docker-compose.yml
index 185d54f..a6aee60 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -18,7 +18,7 @@ version: '2'
 
 services:
   thingsboard:
-    image: "thingsboard/application:1.0"
+    image: "thingsboard/application:1.0.1"
     ports:
       - "8080:8080"
       - "1883:1883"
@@ -27,7 +27,7 @@ services:
       - thingsboard.env
     entrypoint: ./run_thingsboard.sh
   thingsboard-db-schema:
-    image: "thingsboard/thingsboard-db-schema:1.0"
+    image: "thingsboard/thingsboard-db-schema:1.0.1"
     env_file:
       - thingsboard-db-schema.env
     entrypoint: ./install_schema.sh
diff --git a/docker/thingsboard/build_and_deploy.sh b/docker/thingsboard/build_and_deploy.sh
index acbb420..47cf1e8 100755
--- a/docker/thingsboard/build_and_deploy.sh
+++ b/docker/thingsboard/build_and_deploy.sh
@@ -18,8 +18,8 @@
 
 cp ../../application/target/thingsboard.deb thingsboard.deb
 
-docker build -t thingsboard/application:1.0 .
+docker build -t thingsboard/application:1.0.1 .
 
 docker login
 
-docker push thingsboard/application:1.0
\ No newline at end of file
+docker push thingsboard/application:1.0.1
\ No newline at end of file
diff --git a/docker/thingsboard-db-schema/build_and_deploy.sh b/docker/thingsboard-db-schema/build_and_deploy.sh
index 76d8743..a050d83 100755
--- a/docker/thingsboard-db-schema/build_and_deploy.sh
+++ b/docker/thingsboard-db-schema/build_and_deploy.sh
@@ -20,8 +20,8 @@ cp ../../dao/src/main/resources/schema.cql schema.cql
 cp ../../dao/src/main/resources/demo-data.cql demo-data.cql
 cp ../../dao/src/main/resources/system-data.cql system-data.cql
 
-docker build -t thingsboard/thingsboard-db-schema:1.0 .
+docker build -t thingsboard/thingsboard-db-schema:1.0.1 .
 
 docker login
 
-docker push thingsboard/thingsboard-db-schema:1.0
\ No newline at end of file
+docker push thingsboard/thingsboard-db-schema:1.0.1
\ No newline at end of file
diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index d4deb77..0cbf7f2 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index 926c4e6..841a3ae 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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 e166abb..5a5f464 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>extensions</artifactId>
     </parent>
     <groupId>org.thingsboard.extensions</groupId>
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 618e58c..4ae86ee 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index f90a278..951abd7 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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-core/pom.xml b/extensions-core/pom.xml
index bb17d0c..fb9e534 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 06e9821..22fb73f 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.1-SNAPSHOT</version>
+    <version>1.1.0-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <name>Thingsboard</name>

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

diff --git a/tools/pom.xml b/tools/pom.xml
index c1b3cc0..ad5921f 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>thingsboard</artifactId>
     </parent>
     <groupId>org.thingsboard</groupId>
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/transport/coap/pom.xml b/transport/coap/pom.xml
index b4bc06b..f845b7a 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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 8909763..1d93af8 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</version>
         <artifactId>transport</artifactId>
     </parent>
     <groupId>org.thingsboard.transport</groupId>
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 866ea4f..ba7128d 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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 0e1c261..4b7c8ab 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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 2(+2 -0)

diff --git a/ui/package.json b/ui/package.json
index 070c07f..c1101d2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -14,6 +14,7 @@
     "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",
@@ -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 379511d..d72f7ff 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.1-SNAPSHOT</version>
+        <version>1.1.0-SNAPSHOT</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..f90b055 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
     };
 
@@ -159,6 +161,20 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
 
     $scope.$watch('vm.columns', function () {
         vm.gridsterOpts.columns = vm.columns ? vm.columns : 24;
+        if (gridster) {
+            gridster.columns = vm.columns;
+            updateGridsterParams();
+        }
+        updateVisibleRect();
+    });
+
+    $scope.$watch('vm.margins', function () {
+        vm.gridsterOpts.margins = vm.margins ? vm.margins : [10, 10];
+        if (gridster) {
+            gridster.margins = vm.margins;
+            updateGridsterParams();
+        }
+        updateVisibleRect();
     });
 
     $scope.$watch('vm.isEdit', function () {
@@ -224,6 +240,26 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
         }, 0, false);
     }
 
+    function updateGridsterParams() {
+        if (gridster) {
+            if (gridster.colWidth === 'auto') {
+                gridster.curColWidth = (gridster.curWidth + (gridster.outerMargin ? -gridster.margins[1] : gridster.margins[1])) / gridster.columns;
+            } else {
+                gridster.curColWidth = gridster.colWidth;
+            }
+            gridster.curRowHeight = gridster.rowHeight;
+            if (angular.isString(gridster.rowHeight)) {
+                if (gridster.rowHeight === 'match') {
+                    gridster.curRowHeight = Math.round(gridster.curColWidth);
+                } else if (gridster.rowHeight.indexOf('*') !== -1) {
+                    gridster.curRowHeight = Math.round(gridster.curColWidth * gridster.rowHeight.replace('*', '').replace(' ', ''));
+                } else if (gridster.rowHeight.indexOf('/') !== -1) {
+                    gridster.curRowHeight = Math.round(gridster.curColWidth / gridster.rowHeight.replace('/', '').replace(' ', ''));
+                }
+            }
+        }
+    }
+
     function updateVisibleRect (force, containerResized) {
         if (gridster) {
             var position = $(gridster.$element).position()
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/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index 565c8a1..fbc559b 100644
--- a/ui/src/app/components/widget.controller.js
+++ b/ui/src/app/components/widget.controller.js
@@ -159,6 +159,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
     };
 
     vm.gridsterItemInitialized = gridsterItemInitialized;
+    vm.visibleRectChanged = visibleRectChanged;
 
     function gridsterItemInitialized(item) {
         if (item) {
@@ -167,6 +168,11 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
         }
     }
 
+    function visibleRectChanged(newVisibleRect) {
+        visibleRect = newVisibleRect;
+        updateVisibility();
+    }
+
     initWidget();
 
     function initWidget() {
@@ -221,11 +227,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
             $scope.$emit("widgetPositionChanged", widget);
         });
 
-        $scope.$on('visibleRectChanged', function (event, newVisibleRect) {
-            visibleRect = newVisibleRect;
-            updateVisibility();
-        });
-
         $scope.$on('onWidgetFullscreenChanged', function(event, isWidgetExpanded, fullscreenWidget) {
             if (widget === fullscreenWidget) {
                 onRedraw(0);
@@ -318,9 +319,10 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
 
 
     function onRedraw(delay, dataUpdate) {
-        if (!visible) {
+        //TODO:
+        /*if (!visible) {
             return;
-        }
+        }*/
         if (angular.isUndefined(delay)) {
             delay = 0;
         }
diff --git a/ui/src/app/components/widget.directive.js b/ui/src/app/components/widget.directive.js
index 6ba25c3..b6000cb 100644
--- a/ui/src/app/components/widget.directive.js
+++ b/ui/src/app/components/widget.directive.js
@@ -34,12 +34,19 @@ function Widget($controller, $compile, widgetService) {
             var widget = locals.widget;
             var gridsterItem;
 
+            scope.$on('visibleRectChanged', function (event, newVisibleRect) {
+                locals.visibleRect = newVisibleRect;
+                if (widgetController) {
+                    widgetController.visibleRectChanged(newVisibleRect);
+                }
+            });
+
             scope.$on('gridster-item-initialized', function (event, item) {
                 gridsterItem = item;
                 if (widgetController) {
                     widgetController.gridsterItemInitialized(gridsterItem);
                 }
-            })
+            });
 
             elem.html('<div flex layout="column" layout-align="center center" style="height: 100%;">' +
                       '     <md-progress-circular md-mode="indeterminate" class="md-accent md-hue-2" md-diameter="120"></md-progress-circular>' +
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}}"