Commit e276c9a936c7f0e9d16b7145ebf3367c6bd06c52

Authored by Andrii Shvaika
1 parent 46fec515

Initial WebSocker API

... ... @@ -49,6 +49,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
49 49 import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent;
50 50 import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
51 51 import org.thingsboard.server.queue.discovery.PartitionService;
  52 +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
52 53 import org.thingsboard.server.queue.util.TbCoreComponent;
53 54 import org.thingsboard.server.service.queue.TbClusterService;
54 55 import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
... ... @@ -106,18 +107,28 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
106 107 private SubscriptionManagerService subscriptionManagerService;
107 108
108 109 @Autowired
  110 + @Lazy
  111 + private TbLocalSubscriptionService localSubscriptionService;
  112 +
  113 + @Autowired
109 114 private TimeseriesService tsService;
110 115
  116 + @Autowired
  117 + private TbServiceInfoProvider serviceInfoProvider;
  118 +
111 119 @Value("${database.ts.type}")
112 120 private String databaseTsType;
113 121
114 122 private ExecutorService wsCallBackExecutor;
115 123 private boolean tsInSqlDB;
  124 + private String serviceId;
116 125
117 126 @PostConstruct
118 127 public void initExecutor() {
  128 + serviceId = serviceInfoProvider.getServiceId();
119 129 wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback"));
120 130 tsInSqlDB = databaseTsType.equalsIgnoreCase("sql") || databaseTsType.equalsIgnoreCase("timescale");
  131 +
121 132 }
122 133
123 134 @PreDestroy
... ... @@ -158,6 +169,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
158 169 TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
159 170 if (ctx != null) {
160 171 log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd);
  172 + if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) {
  173 + ctx.clearSubscriptions();
  174 + }
161 175 //TODO: cleanup old subscription;
162 176 } else {
163 177 log.debug("[{}][{}] Creating new subscription using: {}", session.getSessionId(), cmd.getCmdId(), cmd);
... ... @@ -209,7 +223,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
209 223
210 224 private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
211 225 Map<Integer, TbEntityDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
212   - TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(sessionRef, cmd.getCmdId());
  226 + TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, sessionRef, cmd.getCmdId());
213 227 ctx.setQuery(cmd.getQuery());
214 228 sessionSubs.put(cmd.getCmdId(), ctx);
215 229 return ctx;
... ... @@ -266,7 +280,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
266 280 update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData());
267 281 }
268 282 wsService.sendWsMsg(ctx.getSessionId(), update);
269   - //TODO: create context for this (session, cmdId) that contains query, latestCmd and update. Subscribe + periodic updates.
  283 + createLatestSubscriptions(ctx, latestCmd);
270 284 }
271 285
272 286 @Override
... ... @@ -281,10 +295,16 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
281 295 EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null);
282 296 wsService.sendWsMsg(ctx.getSessionId(), update);
283 297 }
284   - //TODO: create context for this (session, cmdId) that contains query, latestCmd and update. Subscribe + periodic updates.
  298 + createLatestSubscriptions(ctx, latestCmd);
285 299 }
286 300 }
287 301
  302 + private void createLatestSubscriptions(TbEntityDataSubCtx ctx, LatestValueCmd latestCmd) {
  303 + //TODO: create context for this (session, cmdId) that contains query, latestCmd and update. Subscribe + periodic updates.
  304 + List<TbSubscription> tbSubs = ctx.createSubscriptions(latestCmd.getKeys());
  305 + tbSubs.forEach(sub -> localSubscriptionService.addSubscription(sub));
  306 + }
  307 +
288 308 private Map<String, TsValue> toTsValue(List<TsKvEntry> data) {
289 309 return data.stream().collect(Collectors.toMap(TsKvEntry::getKey, value -> new TsValue(value.getTs(), value.getValueAsString())));
290 310 }
... ...
... ... @@ -5,7 +5,7 @@
5 5 * you may not use this file except in compliance with the License.
6 6 * You may obtain a copy of the License at
7 7 *
8   - * http://www.apache.org/licenses/LICENSE-2.0
  8 + * http://www.apache.org/licenses/LICENSE-2.0
9 9 *
10 10 * Unless required by applicable law or agreed to in writing, software
11 11 * distributed under the License is distributed on an "AS IS" BASIS,
... ... @@ -59,9 +59,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
59 59 private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>();
60 60
61 61 @Autowired
62   - private TelemetryWebSocketService wsService;
63   -
64   - @Autowired
65 62 private EntityViewService entityViewService;
66 63
67 64 @Autowired
... ... @@ -155,7 +152,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
155 152 update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
156 153 break;
157 154 }
158   - wsService.sendWsMsg(sessionId, update);
  155 + subscription.getUpdateConsumer().accept(sessionId, update);
159 156 }
160 157 callback.onSuccess();
161 158 }
... ...
... ... @@ -20,8 +20,10 @@ import lombok.Data;
20 20 import lombok.Getter;
21 21 import org.thingsboard.server.common.data.id.EntityId;
22 22 import org.thingsboard.server.common.data.id.TenantId;
  23 +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
23 24
24 25 import java.util.Map;
  26 +import java.util.function.BiConsumer;
25 27
26 28 public class TbAttributeSubscription extends TbSubscription {
27 29
... ... @@ -31,8 +33,9 @@ public class TbAttributeSubscription extends TbSubscription {
31 33
32 34 @Builder
33 35 public TbAttributeSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
  36 + BiConsumer<String, SubscriptionUpdate> updateConsumer,
34 37 boolean allKeys, Map<String, Long> keyStates, TbAttributeSubscriptionScope scope) {
35   - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES);
  38 + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES, updateConsumer);
36 39 this.allKeys = allKeys;
37 40 this.keyStates = keyStates;
38 41 this.scope = scope;
... ...
... ... @@ -2,17 +2,35 @@ package org.thingsboard.server.service.subscription;
2 2
3 3 import lombok.Data;
4 4 import org.thingsboard.server.common.data.id.CustomerId;
  5 +import org.thingsboard.server.common.data.id.EntityId;
5 6 import org.thingsboard.server.common.data.id.TenantId;
6 7 import org.thingsboard.server.common.data.page.PageData;
7 8 import org.thingsboard.server.common.data.query.EntityData;
8 9 import org.thingsboard.server.common.data.query.EntityDataQuery;
  10 +import org.thingsboard.server.common.data.query.EntityKey;
  11 +import org.thingsboard.server.common.data.query.EntityKeyType;
  12 +import org.thingsboard.server.common.data.query.TsValue;
  13 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
9 14 import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
  15 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
10 16 import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
11 17 import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd;
  18 +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
  19 +
  20 +import java.util.ArrayList;
  21 +import java.util.Arrays;
  22 +import java.util.Collections;
  23 +import java.util.HashMap;
  24 +import java.util.List;
  25 +import java.util.Map;
  26 +import java.util.stream.Collectors;
12 27
13 28 @Data
14 29 public class TbEntityDataSubCtx {
15 30
  31 + public static final int MAX_SUBS_PER_CMD = 1024 * 8;
  32 + private final String serviceId;
  33 + private final TelemetryWebSocketService wsService;
16 34 private final TelemetryWebSocketSessionRef sessionRef;
17 35 private final int cmdId;
18 36 private EntityDataQuery query;
... ... @@ -20,8 +38,13 @@ public class TbEntityDataSubCtx {
20 38 private TimeSeriesCmd tsCmd;
21 39 private PageData<EntityData> data;
22 40 private boolean initialDataSent;
  41 + private List<TbSubscription> tbSubs;
  42 + private int internalSubIdx;
  43 + private Map<Integer, EntityId> subToEntityIdMap;
23 44
24   - public TbEntityDataSubCtx(TelemetryWebSocketSessionRef sessionRef, int cmdId) {
  45 + public TbEntityDataSubCtx(String serviceId, TelemetryWebSocketService wsService, TelemetryWebSocketSessionRef sessionRef, int cmdId) {
  46 + this.serviceId = serviceId;
  47 + this.wsService = wsService;
25 48 this.sessionRef = sessionRef;
26 49 this.cmdId = cmdId;
27 50 }
... ... @@ -38,9 +61,79 @@ public class TbEntityDataSubCtx {
38 61 return sessionRef.getSecurityCtx().getCustomerId();
39 62 }
40 63
41   -
42 64 public void setData(PageData<EntityData> data) {
43 65 this.data = data;
44 66 }
45 67
  68 + public List<TbSubscription> createSubscriptions(List<EntityKey> keys) {
  69 + this.subToEntityIdMap = new HashMap<>();
  70 + this.internalSubIdx = cmdId * MAX_SUBS_PER_CMD;
  71 + tbSubs = new ArrayList<>();
  72 + List<EntityKey> attrSubKeys = new ArrayList<>();
  73 + List<EntityKey> tsSubKeys = new ArrayList<>();
  74 + for (EntityKey key : keys) {
  75 + switch (key.getType()) {
  76 + case TIME_SERIES:
  77 + tsSubKeys.add(key);
  78 + break;
  79 + case ATTRIBUTE:
  80 + case CLIENT_ATTRIBUTE:
  81 + case SHARED_ATTRIBUTE:
  82 + case SERVER_ATTRIBUTE:
  83 + attrSubKeys.add(key);
  84 + }
  85 + }
  86 + for (EntityData entityData : data.getData()) {
  87 + if (!tsSubKeys.isEmpty()) {
  88 + tbSubs.add(createTsSub(entityData, tsSubKeys));
  89 + }
  90 + }
  91 + return tbSubs;
  92 + }
  93 +
  94 + private TbSubscription createTsSub(EntityData entityData, List<EntityKey> tsSubKeys) {
  95 + int subIdx = internalSubIdx++;
  96 + subToEntityIdMap.put(subIdx, entityData.getEntityId());
  97 + Map<String, Long> keyStates = new HashMap<>();
  98 + tsSubKeys.forEach(key -> keyStates.put(key.getKey(), 0L));
  99 + if (entityData.getLatest() != null) {
  100 + Map<String, TsValue> currentValues = entityData.getLatest().get(EntityKeyType.TIME_SERIES);
  101 + if (currentValues != null) {
  102 + currentValues.forEach((k, v) -> keyStates.put(k, v.getTs()));
  103 + }
  104 + }
  105 + if (entityData.getTimeseries() != null) {
  106 + entityData.getTimeseries().forEach((k, v) -> keyStates.put(k, Arrays.stream(v).map(TsValue::getTs).max(Long::compareTo).orElse(0L)));
  107 + }
  108 +
  109 + return TbTimeseriesSubscription.builder()
  110 + .serviceId(serviceId)
  111 + .sessionId(sessionRef.getSessionId())
  112 + .subscriptionId(subIdx)
  113 + .tenantId(sessionRef.getSecurityCtx().getTenantId())
  114 + .entityId(entityData.getEntityId())
  115 + .updateConsumer(this::sendTsWsMsg)
  116 + .allKeys(false)
  117 + .keyStates(keyStates).build();
  118 + }
  119 +
  120 +
  121 + private void sendTsWsMsg(String sessionId, SubscriptionUpdate subscriptionUpdate) {
  122 + EntityId entityId = subToEntityIdMap.get(subscriptionUpdate.getSubscriptionId());
  123 + if (entityId != null) {
  124 + Map<String, TsValue> latest = new HashMap<>();
  125 + subscriptionUpdate.getData().forEach((k, v) -> {
  126 + Object[] data = (Object[]) v.get(0);
  127 + latest.put(k, new TsValue((Long) data[0], (String) data[1]));
  128 + });
  129 + Map<EntityKeyType, Map<String, TsValue>> latestMap = Collections.singletonMap(EntityKeyType.TIME_SERIES, latest);
  130 + EntityData entityData = new EntityData(entityId, latestMap, null);
  131 + wsService.sendWsMsg(sessionId, new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData)));
  132 + }
  133 +
  134 + }
  135 +
  136 + public void clearSubscriptions() {
  137 + subToEntityIdMap.clear();
  138 + }
46 139 }
... ...
... ... @@ -19,8 +19,10 @@ import lombok.AllArgsConstructor;
19 19 import lombok.Data;
20 20 import org.thingsboard.server.common.data.id.EntityId;
21 21 import org.thingsboard.server.common.data.id.TenantId;
  22 +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
22 23
23 24 import java.util.Objects;
  25 +import java.util.function.BiConsumer;
24 26
25 27 @Data
26 28 @AllArgsConstructor
... ... @@ -32,6 +34,7 @@ public abstract class TbSubscription {
32 34 private final TenantId tenantId;
33 35 private final EntityId entityId;
34 36 private final TbSubscriptionType type;
  37 + private final BiConsumer<String, SubscriptionUpdate> updateConsumer;
35 38
36 39 @Override
37 40 public boolean equals(Object o) {
... ...
... ... @@ -5,7 +5,7 @@
5 5 * you may not use this file except in compliance with the License.
6 6 * You may obtain a copy of the License at
7 7 *
8   - * http://www.apache.org/licenses/LICENSE-2.0
  8 + * http://www.apache.org/licenses/LICENSE-2.0
9 9 *
10 10 * Unless required by applicable law or agreed to in writing, software
11 11 * distributed under the License is distributed on an "AS IS" BASIS,
... ... @@ -19,20 +19,27 @@ import lombok.Builder;
19 19 import lombok.Getter;
20 20 import org.thingsboard.server.common.data.id.EntityId;
21 21 import org.thingsboard.server.common.data.id.TenantId;
  22 +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate;
22 23
23 24 import java.util.Map;
  25 +import java.util.function.BiConsumer;
24 26
25 27 public class TbTimeseriesSubscription extends TbSubscription {
26 28
27   - @Getter private final boolean allKeys;
28   - @Getter private final Map<String, Long> keyStates;
29   - @Getter private final long startTime;
30   - @Getter private final long endTime;
  29 + @Getter
  30 + private final boolean allKeys;
  31 + @Getter
  32 + private final Map<String, Long> keyStates;
  33 + @Getter
  34 + private final long startTime;
  35 + @Getter
  36 + private final long endTime;
31 37
32 38 @Builder
33 39 public TbTimeseriesSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
  40 + BiConsumer<String, SubscriptionUpdate> updateConsumer,
34 41 boolean allKeys, Map<String, Long> keyStates, long startTime, long endTime) {
35   - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES);
  42 + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateConsumer);
36 43 this.allKeys = allKeys;
37 44 this.keyStates = keyStates;
38 45 this.startTime = startTime;
... ...
... ... @@ -86,6 +86,7 @@ import java.util.concurrent.ConcurrentHashMap;
86 86 import java.util.concurrent.ConcurrentMap;
87 87 import java.util.concurrent.ExecutorService;
88 88 import java.util.concurrent.Executors;
  89 +import java.util.function.BiConsumer;
89 90 import java.util.function.Consumer;
90 91 import java.util.stream.Collectors;
91 92
... ... @@ -129,6 +130,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
129 130 @Autowired
130 131 private TbServiceInfoProvider serviceInfoProvider;
131 132
  133 +
132 134 @Value("${server.ws.limits.max_subscriptions_per_tenant:0}")
133 135 private int maxSubscriptionsPerTenant;
134 136 @Value("${server.ws.limits.max_subscriptions_per_customer:0}")
... ... @@ -398,7 +400,9 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
398 400 .entityId(entityId)
399 401 .allKeys(false)
400 402 .keyStates(subState)
401   - .scope(scope).build();
  403 + .scope(scope)
  404 + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg)
  405 + .build();
402 406 oldSubService.addSubscription(sub);
403 407 }
404 408
... ... @@ -495,6 +499,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
495 499 .entityId(entityId)
496 500 .allKeys(true)
497 501 .keyStates(subState)
  502 + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg)
498 503 .scope(scope).build();
499 504 oldSubService.addSubscription(sub);
500 505 }
... ... @@ -575,6 +580,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
575 580 .subscriptionId(cmd.getCmdId())
576 581 .tenantId(sessionRef.getSecurityCtx().getTenantId())
577 582 .entityId(entityId)
  583 + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg)
578 584 .allKeys(true)
579 585 .keyStates(subState).build();
580 586 oldSubService.addSubscription(sub);
... ... @@ -612,6 +618,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
612 618 .subscriptionId(cmd.getCmdId())
613 619 .tenantId(sessionRef.getSecurityCtx().getTenantId())
614 620 .entityId(entityId)
  621 + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg)
615 622 .allKeys(false)
616 623 .keyStates(subState).build();
617 624 oldSubService.addSubscription(sub);
... ...
... ... @@ -5,7 +5,7 @@
5 5 * you may not use this file except in compliance with the License.
6 6 * You may obtain a copy of the License at
7 7 *
8   - * http://www.apache.org/licenses/LICENSE-2.0
  8 + * http://www.apache.org/licenses/LICENSE-2.0
9 9 *
10 10 * Unless required by applicable law or agreed to in writing, software
11 11 * distributed under the License is distributed on an "AS IS" BASIS,
... ... @@ -15,6 +15,9 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import com.google.common.util.concurrent.FutureCallback;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.checkerframework.checker.nullness.qual.Nullable;
18 21 import org.junit.After;
19 22 import org.junit.Assert;
20 23 import org.junit.Before;
... ... @@ -38,6 +41,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType;
38 41 import org.thingsboard.server.common.data.query.TsValue;
39 42 import org.thingsboard.server.common.data.security.Authority;
40 43 import org.thingsboard.server.dao.timeseries.TimeseriesService;
  44 +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
41 45 import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
42 46 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
43 47 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
... ... @@ -46,10 +50,13 @@ import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd;
46 50
47 51 import java.util.Arrays;
48 52 import java.util.Collections;
  53 +import java.util.List;
  54 +import java.util.concurrent.CountDownLatch;
49 55 import java.util.concurrent.TimeUnit;
50 56
51 57 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
52 58
  59 +@Slf4j
53 60 public class BaseWebsocketApiTest extends AbstractWebsocketTest {
54 61
55 62 private Tenant savedTenant;
... ... @@ -57,7 +64,7 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
57 64 private TbTestWebSocketClient wsClient;
58 65
59 66 @Autowired
60   - private TimeseriesService tsService;
  67 + private TelemetrySubscriptionService tsService;
61 68
62 69 @Before
63 70 public void beforeTest() throws Exception {
... ... @@ -129,7 +136,10 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
129 136 TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
130 137 TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L));
131 138 TsKvEntry dataPoint3 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(3), new LongDataEntry("temperature", 42L));
132   - tsService.save(device.getTenantId(), device.getId(), Arrays.asList(dataPoint1, dataPoint2, dataPoint3), 0).get();
  139 + List<TsKvEntry> tsData = Arrays.asList(dataPoint1, dataPoint2, dataPoint3);
  140 +
  141 + sendTelemetry(device, tsData);
  142 + Thread.sleep(1000);
133 143
134 144 wsClient.send(mapper.writeValueAsString(wrapper));
135 145 msg = wsClient.waitForReply();
... ... @@ -146,6 +156,22 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
146 156 Assert.assertEquals(new TsValue(dataPoint3.getTs(), dataPoint3.getValueAsString()), tsArray[2]);
147 157 }
148 158
  159 + private void sendTelemetry(Device device, List<TsKvEntry> tsData) throws InterruptedException {
  160 + CountDownLatch latch = new CountDownLatch(1);
  161 + tsService.saveAndNotify(device.getTenantId(), device.getId(), tsData, 0, new FutureCallback<Void>() {
  162 + @Override
  163 + public void onSuccess(@Nullable Void result) {
  164 + latch.countDown();
  165 + }
  166 +
  167 + @Override
  168 + public void onFailure(Throwable t) {
  169 + latch.countDown();
  170 + }
  171 + });
  172 + latch.await(3, TimeUnit.SECONDS);
  173 + }
  174 +
149 175 @Test
150 176 @Ignore
151 177 public void testEntityDataLatestWsCmd() throws Exception {
... ... @@ -177,12 +203,15 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
177 203 Assert.assertNotNull(pageData);
178 204 Assert.assertEquals(1, pageData.getData().size());
179 205 Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId());
180   - Assert.assertNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
  206 + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"));
  207 + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs());
  208 + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
181 209
182 210 TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L));
183   - tsService.save(device.getTenantId(), device.getId(), Arrays.asList(dataPoint1), 0).get();
  211 + List<TsKvEntry> tsData = Arrays.asList(dataPoint1);
  212 + sendTelemetry(device, tsData);
184 213
185   - cmd = new EntityDataCmd(2, edq, null, latestCmd, null);
  214 + cmd = new EntityDataCmd(1, edq, null, latestCmd, null);
186 215
187 216 wrapper = new TelemetryPluginCmdsWrapper();
188 217 wrapper.setEntityDataCmds(Collections.singletonList(cmd));
... ... @@ -190,7 +219,7 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
190 219 wsClient.send(mapper.writeValueAsString(wrapper));
191 220 msg = wsClient.waitForReply();
192 221 update = mapper.readValue(msg, EntityDataUpdate.class);
193   - Assert.assertEquals(2, update.getCmdId());
  222 + Assert.assertEquals(1, update.getCmdId());
194 223 pageData = update.getData();
195 224 Assert.assertNotNull(pageData);
196 225 Assert.assertEquals(1, pageData.getData().size());
... ... @@ -198,6 +227,22 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
198 227 Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES));
199 228 TsValue tsValue = pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
200 229 Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue);
  230 +
  231 + log.error("GOING TO LISTEN FOR UPDATES");
  232 + msg = wsClient.waitForUpdate();
  233 + now = System.currentTimeMillis();
  234 + TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L));
  235 + sendTelemetry(device, Arrays.asList(dataPoint2));
  236 +
  237 + update = mapper.readValue(msg, EntityDataUpdate.class);
  238 + Assert.assertEquals(1, update.getCmdId());
  239 + List<EntityData> eData = update.getUpdate();
  240 + Assert.assertNotNull(eData);
  241 + Assert.assertEquals(1, eData.size());
  242 + Assert.assertEquals(device.getId(), eData.get(0).getEntityId());
  243 + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES));
  244 + tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature");
  245 + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue);
201 246 }
202 247
203 248 }
... ...
... ... @@ -5,7 +5,7 @@
5 5 * you may not use this file except in compliance with the License.
6 6 * You may obtain a copy of the License at
7 7 *
8   - * http://www.apache.org/licenses/LICENSE-2.0
  8 + * http://www.apache.org/licenses/LICENSE-2.0
9 9 *
10 10 * Unless required by applicable law or agreed to in writing, software
11 11 * distributed under the License is distributed on an "AS IS" BASIS,
... ... @@ -27,9 +27,11 @@ import java.util.concurrent.TimeUnit;
27 27 @Slf4j
28 28 public class TbTestWebSocketClient extends WebSocketClient {
29 29
30   - private volatile String lastMsg;
  30 + private volatile String lastReply;
  31 + private volatile String lastUpdate;
31 32 private volatile boolean replyReceived;
32 33 private CountDownLatch reply;
  34 + private CountDownLatch update;
33 35
34 36 public TbTestWebSocketClient(URI serverUri) {
35 37 super(serverUri);
... ... @@ -42,11 +44,22 @@ public class TbTestWebSocketClient extends WebSocketClient {
42 44
43 45 @Override
44 46 public void onMessage(String s) {
45   - if (!replyReceived) {
46   - replyReceived = true;
47   - lastMsg = s;
48   - if (reply != null) {
49   - reply.countDown();
  47 + log.error("RECEIVED: {}", s);
  48 + synchronized (this) {
  49 + if (!replyReceived) {
  50 + replyReceived = true;
  51 + lastReply = s;
  52 + log.error("LAST REPLY: {}", s);
  53 + if (reply != null) {
  54 + reply.countDown();
  55 + }
  56 + } else {
  57 + lastUpdate = s;
  58 + log.error("LAST UPDATE: {}", s);
  59 + if (update == null) {
  60 + update = new CountDownLatch(1);
  61 + }
  62 + update.countDown();
50 63 }
51 64 }
52 65 }
... ... @@ -63,17 +76,31 @@ public class TbTestWebSocketClient extends WebSocketClient {
63 76
64 77 @Override
65 78 public void send(String text) throws NotYetConnectedException {
66   - reply = new CountDownLatch(1);
67   - replyReceived = false;
  79 + synchronized (this) {
  80 + reply = new CountDownLatch(1);
  81 + replyReceived = false;
  82 + }
68 83 super.send(text);
69 84 }
70 85
  86 + public String waitForUpdate() {
  87 + synchronized (this) {
  88 + update = new CountDownLatch(1);
  89 + }
  90 + try {
  91 + update.await(3, TimeUnit.SECONDS);
  92 + } catch (InterruptedException e) {
  93 + log.warn("Failed to await reply", e);
  94 + }
  95 + return lastUpdate;
  96 + }
  97 +
71 98 public String waitForReply() {
72 99 try {
73 100 reply.await(3, TimeUnit.SECONDS);
74 101 } catch (InterruptedException e) {
75 102 log.warn("Failed to await reply", e);
76 103 }
77   - return lastMsg;
  104 + return lastReply;
78 105 }
79 106 }
... ...
... ... @@ -121,7 +121,7 @@ public class EntityKeyMapping {
121 121 String join = hasFilter() ? "left join" : "left outer join";
122 122 ctx.addStringParameter(alias + "_key_id", entityKey.getKey());
123 123 if (entityKey.getType().equals(EntityKeyType.TIME_SERIES)) {
124   - return String.format("%s ts_kv_latest %s ON %s.entity_id=to_uuid(entities.id) AND %s.key = (select key_id from ts_kv_dictionary where key = :%s_key_id)",
  124 + return String.format("%s ts_kv_latest %s ON %s.entity_id=entities.id AND %s.key = (select key_id from ts_kv_dictionary where key = :%s_key_id)",
125 125 join, alias, alias, alias, alias);
126 126 } else {
127 127 String query = String.format("%s attribute_kv %s ON %s.entity_id=entities.id AND %s.entity_type=%s AND %s.attribute_key=:%s_key_id",
... ...