thingsboard-developers

Details

diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
index 7c34aa1..7828267 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
@@ -22,6 +22,7 @@ import javax.annotation.PreDestroy;
 import java.util.concurrent.Executors;
 
 public abstract class JpaAbstractDaoListeningExecutorService {
+
     protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
 
     @PreDestroy
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 7fddfae..170f9a6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
@@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.*;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.PageRequest;
@@ -36,10 +34,12 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
 import org.thingsboard.server.dao.util.SqlDao;
 
 import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@@ -50,6 +50,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
 @SqlDao
 public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
 
+    private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+
     @Autowired
     private TsKvRepository tsKvRepository;
 
@@ -232,7 +234,8 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
         entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
         entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
         entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
-        return service.submit(() -> {
+        log.trace("Saving entity: " + entity);
+        return insertService.submit(() -> {
             tsKvRepository.save(entity);
             return null;
         });
@@ -240,7 +243,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
 
     @Override
     public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
-        return service.submit(() -> null);
+        return insertService.submit(() -> null);
     }
 
     @Override
@@ -254,10 +257,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
         latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
         latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null));
         latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
-        return service.submit(() -> {
+        return insertService.submit(() -> {
             tsKvLatestRepository.save(latestEntity);
             return null;
         });
     }
 
+    @PreDestroy
+    void onDestroy() {
+        insertService.shutdown();
+    }
+
 }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
index 7b527df..7bed03a 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.transport.mqtt.session;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import io.netty.buffer.ByteBuf;
@@ -24,6 +25,7 @@ import io.netty.buffer.UnpooledByteBufAllocator;
 import io.netty.handler.codec.mqtt.*;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.kv.KvEntry;
 import org.thingsboard.server.common.msg.core.*;
 import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
@@ -35,6 +37,7 @@ import org.thingsboard.server.transport.mqtt.MqttTopics;
 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
 
 import java.nio.charset.Charset;
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -83,7 +86,7 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
                 if (responseMsg.isSuccess()) {
                     MsgType requestMsgType = responseMsg.getRequestMsgType();
                     Integer requestId = responseMsg.getRequestId();
-                    if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+                    if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
                         return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
                     }
                 }
@@ -135,40 +138,43 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
         if (responseData.isPresent()) {
             AttributesKVMsg msg = responseData.get();
             if (msg.getClientAttributes() != null) {
-                msg.getClientAttributes().forEach(v -> addValueToJson(result, "value", v));
+                addValues(result, msg.getClientAttributes());
             }
             if (msg.getSharedAttributes() != null) {
-                msg.getSharedAttributes().forEach(v -> addValueToJson(result, "value", v));
+                addValues(result, msg.getSharedAttributes());
             }
         }
         return createMqttPublishMsg(topic, result);
     }
 
+    private void addValues(JsonObject result, List<AttributeKvEntry> kvList) {
+        if (kvList.size() == 1) {
+            addValueToJson(result, "value", kvList.get(0));
+        } else {
+            JsonObject values;
+            if (result.has("values")) {
+                values = result.get("values").getAsJsonObject();
+            } else {
+                values = new JsonObject();
+                result.add("values", values);
+            }
+            kvList.forEach(value -> addValueToJson(values, value.getKey(), value));
+        }
+    }
+
     private void addValueToJson(JsonObject json, String name, KvEntry entry) {
         switch (entry.getDataType()) {
             case BOOLEAN:
-                Optional<Boolean> booleanValue = entry.getBooleanValue();
-                if (booleanValue.isPresent()) {
-                    json.addProperty(name, booleanValue.get());
-                }
+                entry.getBooleanValue().ifPresent(aBoolean -> json.addProperty(name, aBoolean));
                 break;
             case STRING:
-                Optional<String> stringValue = entry.getStrValue();
-                if (stringValue.isPresent()) {
-                    json.addProperty(name, stringValue.get());
-                }
+                entry.getStrValue().ifPresent(aString -> json.addProperty(name, aString));
                 break;
             case DOUBLE:
-                Optional<Double> doubleValue = entry.getDoubleValue();
-                if (doubleValue.isPresent()) {
-                    json.addProperty(name, doubleValue.get());
-                }
+                entry.getDoubleValue().ifPresent(aDouble -> json.addProperty(name, aDouble));
                 break;
             case LONG:
-                Optional<Long> longValue = entry.getLongValue();
-                if (longValue.isPresent()) {
-                    json.addProperty(name, longValue.get());
-                }
+                entry.getLongValue().ifPresent(aLong -> json.addProperty(name, aLong));
                 break;
         }
     }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index 00d1f0c..5bccf49 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -41,10 +41,7 @@ import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload;
@@ -193,13 +190,22 @@ public class GatewaySessionCtx {
             int requestId = jsonObj.get("id").getAsInt();
             String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
             boolean clientScope = jsonObj.get("client").getAsBoolean();
-            String key = jsonObj.get("key").getAsString();
+            Set<String> keys;
+            if (jsonObj.has("key")) {
+                keys = Collections.singleton(jsonObj.get("key").getAsString());
+            } else {
+                JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
+                keys = new HashSet<>();
+                for (JsonElement keyObj : keysArray) {
+                    keys.add(keyObj.getAsString());
+                }
+            }
 
             BasicGetAttributesRequest request;
             if (clientScope) {
-                request = new BasicGetAttributesRequest(requestId, Collections.singleton(key), null);
+                request = new BasicGetAttributesRequest(requestId, keys, null);
             } else {
-                request = new BasicGetAttributesRequest(requestId, null, Collections.singleton(key));
+                request = new BasicGetAttributesRequest(requestId, null, keys);
             }
             GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
             processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
@@ -251,7 +257,7 @@ public class GatewaySessionCtx {
     }
 
     private void ack(MqttPublishMessage msg) {
-        if(msg.variableHeader().messageId() > 0) {
+        if (msg.variableHeader().messageId() > 0) {
             writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId()));
         }
     }
diff --git a/ui/src/app/home/home-links.controller.js b/ui/src/app/home/home-links.controller.js
index 799a0bc..32ae091 100644
--- a/ui/src/app/home/home-links.controller.js
+++ b/ui/src/app/home/home-links.controller.js
@@ -17,7 +17,42 @@
 import './home-links.scss';
 
 /*@ngInject*/
-export default function HomeLinksController($scope, menu) {
+export default function HomeLinksController($scope, $mdMedia, menu) {
+
     var vm = this;
-    vm.model = menu.getHomeSections();
+
+    vm.sectionColspan = sectionColspan;
+
+    $scope.$watch(function() { return $mdMedia('lg'); }, function() {
+        updateColumnCount();
+    });
+
+    $scope.$watch(function() { return $mdMedia('gt-lg'); }, function() {
+        updateColumnCount();
+    });
+
+    updateColumnCount();
+
+    menu.getHomeSections().then((homeSections) => {
+        vm.model = homeSections;
+    });
+
+    function updateColumnCount() {
+        vm.cols = 2;
+        if ($mdMedia('lg')) {
+            vm.cols = 3;
+        }
+        if ($mdMedia('gt-lg')) {
+            vm.cols = 4;
+        }
+    }
+
+    function sectionColspan(section) {
+        var colspan = vm.cols;
+        if (section && section.places && section.places.length <= colspan) {
+            colspan = section.places.length;
+        }
+        return colspan;
+    }
+
 }
diff --git a/ui/src/app/home/home-links.tpl.html b/ui/src/app/home/home-links.tpl.html
index e27f20a..f0e5582 100644
--- a/ui/src/app/home/home-links.tpl.html
+++ b/ui/src/app/home/home-links.tpl.html
@@ -15,8 +15,8 @@
     limitations under the License.
 
 -->
-<md-grid-list class="tb-home-links" md-cols="2" md-cols-lg="3" md-cols-gt-lg="4" md-row-height="280px">
-	<md-grid-tile md-colspan="2" md-colspan-gt-sm="{{section.places.length}}" ng-repeat="section in vm.model">
+<md-grid-list class="tb-home-links" md-cols="{{vm.cols}}" md-row-height="280px">
+	<md-grid-tile md-colspan="2" md-colspan-gt-sm="{{vm.sectionColspan(section)}}" ng-repeat="section in vm.model">
 		<md-card style='width: 100%;'>
 			<md-card-title>
 				<md-card-title-text>
@@ -25,12 +25,12 @@
 			</md-card-title>
 			<md-card-content>
 				<md-grid-list md-row-height="170px" md-cols="{{section.places.length}}" md-cols-gt-md="{{section.places.length}}">
-					 <md-grid-tile class="card-tile" ng-repeat="place in section.places">
-			        	<md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
-			        		<md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
-			   				<span translate>{{place.name}}</span>
-			        	</md-button>
-					 </md-grid-tile>
+					<md-grid-tile class="card-tile" ng-repeat="place in section.places">
+						<md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
+							<md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
+							<span translate>{{place.name}}</span>
+						</md-button>
+					</md-grid-tile>
 				</md-grid-list>
 			</md-card-content>
 		</md-card>