Browse Source

Merge remote-tracking branch 'origin/master' into feature/TB-41

pull/64/head
Andrew Shvayka 10 years ago
parent
commit
d259920d4d
  1. 2
      application/pom.xml
  2. 29
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 2
      application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
  4. 230
      application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
  5. 49
      application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java
  6. 10
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  7. 7
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  8. 33
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  9. 2
      application/src/main/resources/thingsboard.yml
  10. 5
      application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
  11. 2
      common/data/pom.xml
  12. 25
      common/data/src/main/java/org/thingsboard/server/common/data/kv/Aggregation.java
  13. 56
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
  14. 10
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
  15. 2
      common/message/pom.xml
  16. 2
      common/pom.xml
  17. 2
      common/transport/pom.xml
  18. 2
      dao/pom.xml
  19. 42
      dao/src/main/java/org/thingsboard/server/dao/AbstractAsyncDao.java
  20. 55
      dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java
  21. 3
      dao/src/main/java/org/thingsboard/server/dao/Dao.java
  22. 8
      dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java
  23. 13
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
  24. 10
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  25. 63
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java
  26. 19
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  27. 22
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
  28. 23
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java
  29. 8
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java
  30. 34
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  31. 87
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  32. 18
      dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
  33. 193
      dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
  34. 247
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java
  35. 53
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  36. 29
      dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java
  37. 5
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
  38. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  39. 82
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java
  40. 240
      dao/src/main/resources/system-data.cql
  41. 18
      dao/src/test/java/org/thingsboard/server/dao/attributes/BaseAttributesServiceTest.java
  42. 112
      dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java
  43. 2
      dao/src/test/resources/cassandra-test.properties
  44. 4
      docker/docker-compose.yml
  45. 4
      docker/thingsboard-db-schema/build_and_deploy.sh
  46. 4
      docker/thingsboard/build_and_deploy.sh
  47. 2
      extensions-api/pom.xml
  48. 26
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java
  49. 32
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java
  50. 22
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java
  51. 18
      extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
  52. 2
      extensions-core/pom.xml
  53. 42
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
  54. 51
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
  55. 51
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
  56. 42
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
  57. 16
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
  58. 74
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/BiPluginCallBack.java
  59. 121
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
  60. 37
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
  61. 196
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
  62. 36
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
  63. 2
      extensions/extension-kafka/pom.xml
  64. 2
      extensions/extension-rabbitmq/pom.xml
  65. 2
      extensions/extension-rest-api-call/pom.xml
  66. 2
      extensions/pom.xml
  67. 2
      pom.xml
  68. 2
      tools/pom.xml
  69. 2
      transport/coap/pom.xml
  70. 2
      transport/http/pom.xml
  71. 2
      transport/mqtt/pom.xml
  72. 4
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
  73. 2
      transport/pom.xml
  74. 10
      ui/package.json
  75. 2
      ui/pom.xml
  76. 2
      ui/src/app/admin/general-settings.tpl.html
  77. 2
      ui/src/app/admin/outgoing-mail-settings.tpl.html
  78. 16
      ui/src/app/api/dashboard.service.js
  79. 274
      ui/src/app/api/data-aggregator.js
  80. 309
      ui/src/app/api/datasource.service.js
  81. 213
      ui/src/app/api/device.service.js
  82. 28
      ui/src/app/api/telemetry-websocket.service.js
  83. 330
      ui/src/app/api/time.service.js
  84. 62
      ui/src/app/api/user.service.js
  85. 176
      ui/src/app/api/widget.service.js
  86. 23
      ui/src/app/app.config.js
  87. 7
      ui/src/app/app.js
  88. 55
      ui/src/app/app.run.js
  89. 50
      ui/src/app/common/raf.provider.js
  90. 44
      ui/src/app/common/types.constant.js
  91. 20
      ui/src/app/common/utils.service.js
  92. 2
      ui/src/app/component/component-dialog.tpl.html
  93. 27
      ui/src/app/components/dashboard-select.directive.js
  94. 247
      ui/src/app/components/dashboard.directive.js
  95. 6
      ui/src/app/components/dashboard.scss
  96. 39
      ui/src/app/components/dashboard.tpl.html
  97. 2
      ui/src/app/components/datakey-config-dialog.tpl.html
  98. 217
      ui/src/app/components/device-filter.directive.js
  99. 45
      ui/src/app/components/device-filter.scss
  100. 67
      ui/src/app/components/device-filter.tpl.html

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

29
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -58,6 +58,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -85,14 +86,22 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
this.attributeSubscriptions = new HashMap<>(); this.attributeSubscriptions = new HashMap<>();
this.rpcSubscriptions = new HashMap<>(); this.rpcSubscriptions = new HashMap<>();
this.rpcPendingMap = new HashMap<>(); this.rpcPendingMap = new HashMap<>();
refreshAttributes(); initAttributes();
} }
private void refreshAttributes() { private void initAttributes() {
this.deviceAttributes = new DeviceAttributes(fetchAttributes(DataConstants.CLIENT_SCOPE), this.deviceAttributes = new DeviceAttributes(fetchAttributes(DataConstants.CLIENT_SCOPE),
fetchAttributes(DataConstants.SERVER_SCOPE), fetchAttributes(DataConstants.SHARED_SCOPE)); fetchAttributes(DataConstants.SERVER_SCOPE), fetchAttributes(DataConstants.SHARED_SCOPE));
} }
private void refreshAttributes(DeviceAttributesEventNotificationMsg msg) {
if (msg.isDeleted()) {
msg.getDeletedKeys().forEach(key -> deviceAttributes.remove(key));
} else {
deviceAttributes.update(msg.getScope(), msg.getValues());
}
}
void processRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg) { void processRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg) {
ToDeviceRpcRequest request = msg.getMsg(); ToDeviceRpcRequest request = msg.getMsg();
ToDeviceRpcRequestBody body = request.getBody(); ToDeviceRpcRequestBody body = request.getBody();
@ -195,10 +204,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
} }
void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) { void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
//TODO: improve this procedure to fetch only changed attributes. refreshAttributes(msg);
refreshAttributes(); Set<AttributeKey> keys = msg.getDeletedKeys();
//TODO: support attributes deletion
Set<AttributeKey> keys = msg.getKeys();
if (attributeSubscriptions.size() > 0) { if (attributeSubscriptions.size() > 0) {
ToDeviceMsg notification = null; ToDeviceMsg notification = null;
if (msg.isDeleted()) { if (msg.isDeleted()) {
@ -360,8 +367,14 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
} }
} }
private List<AttributeKvEntry> fetchAttributes(String attributeType) { private List<AttributeKvEntry> fetchAttributes(String scope) {
return systemContext.getAttributesService().findAll(this.deviceId, attributeType); try {
//TODO: replace this with async operation. Happens only during actor creation, but is still criticla for performance,
return systemContext.getAttributesService().findAll(this.deviceId, scope).get();
} catch (InterruptedException | ExecutionException e) {
logger.warning("[{}] Failed to fetch attributes for scope: {}", deviceId, scope);
throw new RuntimeException(e);
}
} }
public void processCredentialsUpdate(ActorContext context, DeviceCredentialsUpdateNotificationMsg msg) { public void processCredentialsUpdate(ActorContext context, DeviceCredentialsUpdateNotificationMsg msg) {

2
application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java

@ -173,7 +173,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
@Override @Override
public void onUpdate(ActorContext context) throws Exception { public void onUpdate(ActorContext context) throws Exception {
PluginMetaData oldPluginMd = systemContext.getPluginService().findPluginById(entityId); PluginMetaData oldPluginMd = pluginMd;
pluginMd = systemContext.getPluginService().findPluginById(entityId); pluginMd = systemContext.getPluginService().findPluginById(entityId);
boolean requiresRestart = false; boolean requiresRestart = false;
logger.info("[{}] Plugin configuration was updated from {} to {}.", entityId, oldPluginMd, pluginMd); logger.info("[{}] Plugin configuration was updated from {} to {}.", entityId, oldPluginMd, pluginMd);

230
application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java

@ -17,6 +17,7 @@ package org.thingsboard.server.actors.plugin;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -55,6 +56,7 @@ import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRe
import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
import akka.actor.ActorRef; import akka.actor.ActorRef;
import org.w3c.dom.Attr;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -88,109 +90,120 @@ public final class PluginProcessingContext implements PluginContext {
} }
@Override @Override
public void saveAttributes(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes, PluginCallback<Void> callback) { public void saveAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List<AttributeKvEntry> attributes, final PluginCallback<Void> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
Set<AttributeKey> keys = new HashSet<>(); ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes);
for (AttributeKvEntry attribute : attributes) { Futures.addCallback(rsListFuture, getListCallback(callback, v -> {
keys.add(new AttributeKey(scope, attribute.getKey())); onDeviceAttributesChanged(tenantId, deviceId, scope, attributes);
} return null;
}), executor);
}));
}
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes); @Override
Futures.addCallback(rsListFuture, getListCallback(callback, v -> { public void removeAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List<String> keys, final PluginCallback<Void> callback) {
onDeviceAttributesChanged(deviceId, keys); validate(deviceId, new ValidationCallback(callback, ctx -> {
return null; ListenableFuture<List<ResultSet>> future = pluginCtx.attributesService.removeAll(deviceId, scope, keys);
}), executor); Futures.addCallback(future, getCallback(callback, v -> null), executor);
onDeviceAttributesDeleted(tenantId, deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet()));
}));
} }
@Override @Override
public Optional<AttributeKvEntry> loadAttribute(DeviceId deviceId, String attributeType, String attributeKey) { public void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, final PluginCallback<Optional<AttributeKvEntry>> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey); ListenableFuture<Optional<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
return Optional.ofNullable(attribute); Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
} }
@Override @Override
public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType, List<String> attributeKeys) { public void loadAttributes(DeviceId deviceId, String attributeType, Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
List<AttributeKvEntry> result = new ArrayList<>(attributeKeys.size()); ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys);
for (String attributeKey : attributeKeys) { Futures.addCallback(future, getCallback(callback, v -> v), executor);
AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey); }));
if (attribute != null) {
result.add(attribute);
}
}
return result;
} }
@Override @Override
public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType) { public void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
return pluginCtx.attributesService.findAll(deviceId, attributeType); ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.findAll(deviceId, attributeType);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
} }
@Override @Override
public void removeAttributes(DeviceId deviceId, String scope, List<String> keys) { public void loadAttributes(final DeviceId deviceId, final Collection<String> attributeTypes, final PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
pluginCtx.attributesService.removeAll(deviceId, scope, keys); List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
onDeviceAttributesDeleted(deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet())); attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.findAll(deviceId, attributeType)));
convertFuturesAndAddCallback(callback, futures);
}));
} }
@Override @Override
public void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback) { public void loadAttributes(final DeviceId deviceId, final Collection<String> attributeTypes, final Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry); List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor); attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys)));
convertFuturesAndAddCallback(callback, futures);
}));
} }
@Override @Override
public void saveTsData(DeviceId deviceId, List<TsKvEntry> entries, PluginCallback<Void> callback) { public void saveTsData(final DeviceId deviceId, final TsKvEntry entry, final PluginCallback<Void> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries); ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor); Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
}));
} }
@Override @Override
public List<TsKvEntry> loadTimeseries(DeviceId deviceId, TsKvQuery query) { public void saveTsData(final DeviceId deviceId, final List<TsKvEntry> entries, final PluginCallback<Void> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
return pluginCtx.tsService.find(DataConstants.DEVICE, deviceId, query); ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
}));
} }
@Override @Override
public void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback) { public void loadTimeseries(final DeviceId deviceId, final List<TsKvQuery> queries, final PluginCallback<List<TsKvEntry>> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId); ListenableFuture<List<TsKvEntry>> future = pluginCtx.tsService.findAll(DataConstants.DEVICE, deviceId, queries);
Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor); Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
} }
@Override @Override
public void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback) { public void loadLatestTimeseries(final DeviceId deviceId, final PluginCallback<List<TsKvEntry>> callback) {
validate(deviceId); validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(DataConstants.DEVICE, deviceId, keys); ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId);
Futures.addCallback(rsListFuture, getListCallback(callback, rsList -> Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor);
{ }));
List<TsKvEntry> result = new ArrayList<>();
for (ResultSet rs : rsList) {
Row row = rs.one();
if (row != null) {
result.add(pluginCtx.tsService.convertResultToTsKvEntry(row));
}
}
return result;
}), executor);
} }
@Override @Override
public void reply(PluginToRuleMsg<?> msg) { public void loadLatestTimeseries(final DeviceId deviceId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
pluginCtx.parentActor.tell(msg, ActorRef.noSender()); validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(DataConstants.DEVICE, deviceId, keys);
Futures.addCallback(rsListFuture, getListCallback(callback, rsList ->
{
List<TsKvEntry> result = new ArrayList<>();
for (ResultSet rs : rsList) {
Row row = rs.one();
if (row != null) {
result.add(pluginCtx.tsService.convertResultToTsKvEntry(row));
}
}
return result;
}), executor);
}));
} }
@Override @Override
public boolean checkAccess(DeviceId deviceId) { public void reply(PluginToRuleMsg<?> msg) {
try { pluginCtx.parentActor.tell(msg, ActorRef.noSender());
return validate(deviceId);
} catch (IllegalStateException | IllegalArgumentException e) {
return false;
}
} }
@Override @Override
@ -203,18 +216,12 @@ public final class PluginProcessingContext implements PluginContext {
return securityCtx; return securityCtx;
} }
private void onDeviceAttributesChanged(DeviceId deviceId, AttributeKey key) { private void onDeviceAttributesDeleted(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) {
onDeviceAttributesChanged(deviceId, Collections.singleton(key)); pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onDelete(tenantId, deviceId, keys));
} }
private void onDeviceAttributesDeleted(DeviceId deviceId, Set<AttributeKey> keys) { private void onDeviceAttributesChanged(TenantId tenantId, DeviceId deviceId, String scope, List<AttributeKvEntry> values) {
Device device = pluginCtx.deviceService.findDeviceById(deviceId); pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, deviceId, scope, values));
pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onDelete(device.getTenantId(), deviceId, keys));
}
private void onDeviceAttributesChanged(DeviceId deviceId, Set<AttributeKey> keys) {
Device device = pluginCtx.deviceService.findDeviceById(deviceId);
pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onUpdate(device.getTenantId(), deviceId, keys));
} }
private <T> FutureCallback<List<ResultSet>> getListCallback(final PluginCallback<T> callback, Function<List<ResultSet>, T> transformer) { private <T> FutureCallback<List<ResultSet>> getListCallback(final PluginCallback<T> callback, Function<List<ResultSet>, T> transformer) {
@ -235,11 +242,15 @@ public final class PluginProcessingContext implements PluginContext {
}; };
} }
private <T> FutureCallback<ResultSet> getCallback(final PluginCallback<T> callback, Function<ResultSet, T> transformer) { private <T, R> FutureCallback<R> getCallback(final PluginCallback<T> callback, Function<R, T> transformer) {
return new FutureCallback<ResultSet>() { return new FutureCallback<R>() {
@Override @Override
public void onSuccess(@Nullable ResultSet result) { public void onSuccess(@Nullable R result) {
pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, transformer.apply(result)), ActorRef.noSender()); try {
pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, transformer.apply(result)), ActorRef.noSender());
} catch (Exception e) {
pluginCtx.self().tell(PluginCallbackMessage.onError(callback, e), ActorRef.noSender());
}
} }
@Override @Override
@ -253,26 +264,35 @@ public final class PluginProcessingContext implements PluginContext {
}; };
} }
// TODO: replace with our own exceptions @Override
private boolean validate(DeviceId deviceId) { public void checkAccess(DeviceId deviceId, PluginCallback<Void> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> callback.onSuccess(ctx, null)));
}
private void validate(DeviceId deviceId, ValidationCallback callback) {
if (securityCtx.isPresent()) { if (securityCtx.isPresent()) {
PluginApiCallSecurityContext ctx = securityCtx.get(); final PluginApiCallSecurityContext ctx = securityCtx.get();
if (ctx.isTenantAdmin() || ctx.isCustomerUser()) { if (ctx.isTenantAdmin() || ctx.isCustomerUser()) {
Device device = pluginCtx.deviceService.findDeviceById(deviceId); ListenableFuture<Device> deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(deviceId);
if (device == null) { Futures.addCallback(deviceFuture, getCallback(callback, device -> {
throw new IllegalStateException("Device not found!"); if (device == null) {
} else { return Boolean.FALSE;
if (!device.getTenantId().equals(ctx.getTenantId())) { } else {
throw new IllegalArgumentException("Device belongs to different tenant!"); if (!device.getTenantId().equals(ctx.getTenantId())) {
} else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) { return Boolean.FALSE;
throw new IllegalArgumentException("Device belongs to different customer!"); } else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) {
return Boolean.FALSE;
} else {
return Boolean.TRUE;
}
} }
} }));
} else { } else {
return false; callback.onSuccess(this, Boolean.FALSE);
} }
} else {
callback.onSuccess(this, Boolean.TRUE);
} }
return true;
} }
@Override @Override
@ -282,9 +302,8 @@ public final class PluginProcessingContext implements PluginContext {
@Override @Override
public void getDevice(DeviceId deviceId, PluginCallback<Device> callback) { public void getDevice(DeviceId deviceId, PluginCallback<Device> callback) {
//TODO: add caching here with async api. ListenableFuture<Device> deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(deviceId);
Device device = pluginCtx.deviceService.findDeviceById(deviceId); Futures.addCallback(deviceFuture, getCallback(callback, v -> v));
pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, device), ActorRef.noSender());
} }
@Override @Override
@ -303,4 +322,15 @@ public final class PluginProcessingContext implements PluginContext {
public void scheduleTimeoutMsg(TimeoutMsg msg) { public void scheduleTimeoutMsg(TimeoutMsg msg) {
pluginCtx.scheduleTimeoutMsg(msg); pluginCtx.scheduleTimeoutMsg(msg);
} }
private void convertFuturesAndAddCallback(PluginCallback<List<AttributeKvEntry>> callback, List<ListenableFuture<List<AttributeKvEntry>>> futures) {
ListenableFuture<List<AttributeKvEntry>> future = Futures.transform(Futures.successfulAsList(futures),
(Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
List<AttributeKvEntry> result = new ArrayList<>();
input.forEach(r -> result.addAll(r));
return result;
}, executor);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}
} }

49
application/src/main/java/org/thingsboard/server/actors/plugin/ValidationCallback.java

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.plugin;
import com.hazelcast.util.function.Consumer;
import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
/**
* Created by ashvayka on 21.02.17.
*/
public class ValidationCallback implements PluginCallback<Boolean> {
private final PluginCallback<?> callback;
private final Consumer<PluginContext> action;
public ValidationCallback(PluginCallback<?> callback, Consumer<PluginContext> action) {
this.callback = callback;
this.action = action;
}
@Override
public void onSuccess(PluginContext ctx, Boolean value) {
if (value) {
action.accept(ctx);
} else {
onFailure(ctx, new UnauthorizedException());
}
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
callback.onFailure(ctx, e);
}
}

10
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -157,6 +157,16 @@ public abstract class BaseController {
} }
} }
void checkArrayParameter(String name, String[] params) throws ThingsboardException {
if (params == null || params.length == 0) {
throw new ThingsboardException("Parameter '" + name + "' can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
for (String param : params) {
checkParameter(name, param);
}
}
}
UUID toUUID(String id) { UUID toUUID(String id) {
return UUID.fromString(id); return UUID.fromString(id);
} }

7
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -32,6 +32,13 @@ import org.thingsboard.server.exception.ThingsboardException;
@RequestMapping("/api") @RequestMapping("/api")
public class DashboardController extends BaseController { public class DashboardController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET)
@ResponseBody
public long getServerTime() throws ThingsboardException {
return System.currentTimeMillis();
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET) @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
@ResponseBody @ResponseBody

33
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -22,6 +23,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -29,6 +31,11 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.exception.ThingsboardException; import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@ -189,4 +196,30 @@ public class DeviceController extends BaseController {
throw handleException(e); throw handleException(e);
} }
} }
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
@ResponseBody
public List<Device> getDevicesByIds(
@RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException {
checkArrayParameter("deviceIds", strDeviceIds);
try {
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
CustomerId customerId = user.getCustomerId();
List<DeviceId> deviceIds = new ArrayList<>();
for (String strDeviceId : strDeviceIds) {
deviceIds.add(new DeviceId(toUUID(strDeviceId)));
}
ListenableFuture<List<Device>> devices;
if (customerId == null || customerId.isNullUid()) {
devices = deviceService.findDevicesByTenantIdAndIdsAsync(tenantId, deviceIds);
} else {
devices = deviceService.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId, customerId, deviceIds);
}
return checkNotNull(devices.get());
} catch (Exception e) {
throw handleException(e);
}
}
} }

2
application/src/main/resources/thingsboard.yml

@ -140,7 +140,7 @@ cassandra:
# Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
# Specify max data points per request # Specify max data points per request
max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:86400}" min_aggregation_step_ms: "${TS_KV_MIN_AGGREGATION_STEP_MS:100}"
# Actor system parameters # Actor system parameters
actors: actors:

5
application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java

@ -22,6 +22,7 @@ import static org.mockito.Mockito.when;
import java.util.*; import java.util.*;
import com.google.common.util.concurrent.Futures;
import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -226,7 +227,9 @@ public class DefaultActorServiceTest {
when(pluginMock.getConfiguration()).thenReturn(pluginAdditionalInfo); when(pluginMock.getConfiguration()).thenReturn(pluginAdditionalInfo);
when(pluginMock.getClazz()).thenReturn(TelemetryStoragePlugin.class.getName()); when(pluginMock.getClazz()).thenReturn(TelemetryStoragePlugin.class.getName());
when(attributesService.findAll(deviceId, DataConstants.CLIENT_SCOPE)).thenReturn(Collections.emptyList()); when(attributesService.findAll(deviceId, DataConstants.CLIENT_SCOPE)).thenReturn(Futures.immediateFuture(Collections.emptyList()));
when(attributesService.findAll(deviceId, DataConstants.SHARED_SCOPE)).thenReturn(Futures.immediateFuture(Collections.emptyList()));
when(attributesService.findAll(deviceId, DataConstants.SERVER_SCOPE)).thenReturn(Futures.immediateFuture(Collections.emptyList()));
initActorSystem(); initActorSystem();
Thread.sleep(1000); Thread.sleep(1000);

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

25
common/data/src/main/java/org/thingsboard/server/common/data/kv/Aggregation.java

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.kv;
/**
* Created by ashvayka on 20.02.17.
*/
public enum Aggregation {
MIN, MAX, AVG, SUM, COUNT, NONE;
}

56
common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java

@ -15,59 +15,29 @@
*/ */
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import java.util.Optional; import lombok.Data;
@Data
public class BaseTsKvQuery implements TsKvQuery { public class BaseTsKvQuery implements TsKvQuery {
private String key; private final String key;
private Optional<Long> startTs; private final long startTs;
private Optional<Long> endTs; private final long endTs;
private Optional<Integer> limit; private final long interval;
private final int limit;
private final Aggregation aggregation;
public BaseTsKvQuery(String key, Optional<Long> startTs, Optional<Long> endTs, Optional<Integer> limit) { public BaseTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation) {
this.key = key; this.key = key;
this.startTs = startTs; this.startTs = startTs;
this.endTs = endTs; this.endTs = endTs;
this.interval = interval;
this.limit = limit; this.limit = limit;
} this.aggregation = aggregation;
public BaseTsKvQuery(String key, Long startTs, Long endTs, Integer limit) {
this(key, Optional.ofNullable(startTs), Optional.ofNullable(endTs), Optional.ofNullable(limit));
}
public BaseTsKvQuery(String key, Long startTs, Integer limit) {
this(key, startTs, null, limit);
}
public BaseTsKvQuery(String key, Long startTs, Long endTs) {
this(key, startTs, endTs, null);
}
public BaseTsKvQuery(String key, Long startTs) {
this(key, startTs, null, null);
} }
public BaseTsKvQuery(String key, Integer limit) { public BaseTsKvQuery(String key, long startTs, long endTs) {
this(key, null, null, limit); this(key, startTs, endTs, endTs-startTs, 1, Aggregation.AVG);
} }
@Override
public String getKey() {
return key;
}
@Override
public Optional<Long> getStartTs() {
return startTs;
}
@Override
public Optional<Long> getEndTs() {
return endTs;
}
@Override
public Optional<Integer> getLimit() {
return limit;
}
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java

@ -21,10 +21,14 @@ public interface TsKvQuery {
String getKey(); String getKey();
Optional<Long> getStartTs(); long getStartTs();
Optional<Long> getEndTs(); long getEndTs();
Optional<Integer> getLimit(); long getInterval();
int getLimit();
Aggregation getAggregation();
} }

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

42
dao/src/main/java/org/thingsboard/server/dao/AbstractAsyncDao.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by ashvayka on 21.02.17.
*/
public abstract class AbstractAsyncDao extends AbstractDao {
protected ExecutorService readResultsProcessingExecutor;
@PostConstruct
public void startExecutor() {
readResultsProcessingExecutor = Executors.newCachedThreadPool();
}
@PreDestroy
public void stopExecutor() {
if (readResultsProcessingExecutor != null) {
readResultsProcessingExecutor.shutdownNow();
}
}
}

55
dao/src/main/java/org/thingsboard/server/dao/AbstractModelDao.java

@ -16,17 +16,22 @@
package org.thingsboard.server.dao; package org.thingsboard.server.dao;
import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Statement; import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.core.utils.UUIDs;
import com.datastax.driver.mapping.Mapper; import com.datastax.driver.mapping.Mapper;
import com.datastax.driver.mapping.Result; import com.datastax.driver.mapping.Result;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.wrapper.EntityResultSet; import org.thingsboard.server.dao.model.wrapper.EntityResultSet;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import javax.annotation.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -59,6 +64,27 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
return list; return list;
} }
protected ListenableFuture<List<T>> findListByStatementAsync(Statement statement) {
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
ListenableFuture<List<T>> result = Futures.transform(resultSetFuture, new Function<ResultSet, List<T>>() {
@Nullable
@Override
public List<T> apply(@Nullable ResultSet resultSet) {
Result<T> result = getMapper().map(resultSet);
if (result != null) {
return result.all();
} else {
return Collections.emptyList();
}
}
});
return result;
}
return Futures.immediateFuture(Collections.emptyList());
}
protected T findOneByStatement(Statement statement) { protected T findOneByStatement(Statement statement) {
T object = null; T object = null;
if (statement != null) { if (statement != null) {
@ -72,6 +98,27 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
return object; return object;
} }
protected ListenableFuture<T> findOneByStatementAsync(Statement statement) {
if (statement != null) {
statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
ListenableFuture<T> result = Futures.transform(resultSetFuture, new Function<ResultSet, T>() {
@Nullable
@Override
public T apply(@Nullable ResultSet resultSet) {
Result<T> result = getMapper().map(resultSet);
if (result != null) {
return result.one();
} else {
return null;
}
}
});
return result;
}
return Futures.immediateFuture(null);
}
protected Statement getSaveQuery(T dto) { protected Statement getSaveQuery(T dto) {
return getMapper().saveQuery(dto); return getMapper().saveQuery(dto);
} }
@ -100,6 +147,14 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
return findOneByStatement(query); return findOneByStatement(query);
} }
public ListenableFuture<T> findByIdAsync(UUID key) {
log.debug("Get entity by key {}", key);
Select.Where query = select().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
log.trace("Execute query {}", query);
return findOneByStatementAsync(query);
}
public ResultSet removeById(UUID key) { public ResultSet removeById(UUID key) {
Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key)); Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
log.debug("Remove request: {}", delete.toString()); log.debug("Remove request: {}", delete.toString());

3
dao/src/main/java/org/thingsboard/server/dao/Dao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao; package org.thingsboard.server.dao;
import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.ResultSet;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -26,6 +27,8 @@ public interface Dao<T> {
T findById(UUID id); T findById(UUID id);
ListenableFuture<T> findByIdAsync(UUID id);
T save(T t); T save(T t);
ResultSet removeById(UUID id); ResultSet removeById(UUID id);

8
dao/src/main/java/org/thingsboard/server/dao/DaoUtil.java

@ -56,4 +56,12 @@ public abstract class DaoUtil {
return id; return id;
} }
public static List<UUID> toUUIDs(List<? extends UUIDBased> idBasedIds) {
List<UUID> ids = new ArrayList<>();
for (UUIDBased idBased : idBasedIds) {
ids.add(getId(idBased));
}
return ids;
}
} }

13
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java

@ -15,23 +15,28 @@
*/ */
package org.thingsboard.server.dao.attributes; package org.thingsboard.server.dao.attributes;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.ResultSetFuture;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.Optional;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
public interface AttributesDao { public interface AttributesDao {
AttributeKvEntry find(EntityId entityId, String attributeType, String attributeKey); ListenableFuture<Optional<AttributeKvEntry>> find(EntityId entityId, String attributeType, String attributeKey);
List<AttributeKvEntry> findAll(EntityId entityId, String attributeType); ListenableFuture<List<AttributeKvEntry>> find(EntityId entityId, String attributeType, Collection<String> attributeKey);
ListenableFuture<List<AttributeKvEntry>> findAll(EntityId entityId, String attributeType);
ResultSetFuture save(EntityId entityId, String attributeType, AttributeKvEntry attribute); ResultSetFuture save(EntityId entityId, String attributeType, AttributeKvEntry attribute);
void removeAll(EntityId entityId, String scope, List<String> keys); ListenableFuture<List<ResultSet>> removeAll(EntityId entityId, String scope, List<String> keys);
} }

10
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -23,18 +23,22 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
public interface AttributesService { public interface AttributesService {
AttributeKvEntry find(EntityId entityId, String scope, String attributeKey); ListenableFuture<Optional<AttributeKvEntry>> find(EntityId entityId, String scope, String attributeKey);
List<AttributeKvEntry> findAll(EntityId entityId, String scope); ListenableFuture<List<AttributeKvEntry>> find(EntityId entityId, String scope, Collection<String> attributeKeys);
ListenableFuture<List<AttributeKvEntry>> findAll(EntityId entityId, String scope);
ListenableFuture<List<ResultSet>> save(EntityId entityId, String scope, List<AttributeKvEntry> attributes); ListenableFuture<List<ResultSet>> save(EntityId entityId, String scope, List<AttributeKvEntry> attributes);
void removeAll(EntityId entityId, String scope, List<String> attributeKeys); ListenableFuture<List<ResultSet>> removeAll(EntityId entityId, String scope, List<String> attributeKeys);
} }

63
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesDao.java

@ -18,19 +18,24 @@ package org.thingsboard.server.dao.attributes;
import com.datastax.driver.core.*; import com.datastax.driver.core.*;
import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.dao.AbstractAsyncDao;
import org.thingsboard.server.dao.AbstractDao;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.dao.timeseries.BaseTimeseriesDao; import org.thingsboard.server.dao.timeseries.BaseTimeseriesDao;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.model.ModelConstants.*; import static org.thingsboard.server.dao.model.ModelConstants.*;
import static com.datastax.driver.core.querybuilder.QueryBuilder.*; import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
@ -40,29 +45,55 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
*/ */
@Component @Component
@Slf4j @Slf4j
public class BaseAttributesDao extends AbstractDao implements AttributesDao { public class BaseAttributesDao extends AbstractAsyncDao implements AttributesDao {
private PreparedStatement saveStmt; private PreparedStatement saveStmt;
@PostConstruct
public void init() {
super.startExecutor();
}
@PreDestroy
public void stop() {
super.stopExecutor();
}
@Override @Override
public AttributeKvEntry find(EntityId entityId, String attributeType, String attributeKey) { public ListenableFuture<Optional<AttributeKvEntry>> find(EntityId entityId, String attributeType, String attributeKey) {
Select.Where select = select().from(ATTRIBUTES_KV_CF) Select.Where select = select().from(ATTRIBUTES_KV_CF)
.where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType())) .where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType()))
.and(eq(ENTITY_ID_COLUMN, entityId.getId())) .and(eq(ENTITY_ID_COLUMN, entityId.getId()))
.and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType)) .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType))
.and(eq(ATTRIBUTE_KEY_COLUMN, attributeKey)); .and(eq(ATTRIBUTE_KEY_COLUMN, attributeKey));
log.trace("Generated query [{}] for entityId {} and key {}", select, entityId, attributeKey); log.trace("Generated query [{}] for entityId {} and key {}", select, entityId, attributeKey);
return convertResultToAttributesKvEntry(attributeKey, executeRead(select).one()); return Futures.transform(executeAsyncRead(select), (Function<? super ResultSet, ? extends Optional<AttributeKvEntry>>) input ->
Optional.ofNullable(convertResultToAttributesKvEntry(attributeKey, input.one()))
, readResultsProcessingExecutor);
} }
@Override @Override
public List<AttributeKvEntry> findAll(EntityId entityId, String attributeType) { public ListenableFuture<List<AttributeKvEntry>> find(EntityId entityId, String attributeType, Collection<String> attributeKeys) {
List<ListenableFuture<Optional<AttributeKvEntry>>> entries = new ArrayList<>();
attributeKeys.forEach(attributeKey -> entries.add(find(entityId, attributeType, attributeKey)));
return Futures.transform(Futures.allAsList(entries), (Function<List<Optional<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
List<AttributeKvEntry> result = new ArrayList<>();
input.stream().filter(opt -> opt.isPresent()).forEach(opt -> result.add(opt.get()));
return result;
}, readResultsProcessingExecutor);
}
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(EntityId entityId, String attributeType) {
Select.Where select = select().from(ATTRIBUTES_KV_CF) Select.Where select = select().from(ATTRIBUTES_KV_CF)
.where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType())) .where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType()))
.and(eq(ENTITY_ID_COLUMN, entityId.getId())) .and(eq(ENTITY_ID_COLUMN, entityId.getId()))
.and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType)); .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType));
log.trace("Generated query [{}] for entityId {} and attributeType {}", select, entityId, attributeType); log.trace("Generated query [{}] for entityId {} and attributeType {}", select, entityId, attributeType);
return convertResultToAttributesKvEntryList(executeRead(select)); return Futures.transform(executeAsyncRead(select), (Function<? super ResultSet, ? extends List<AttributeKvEntry>>) input ->
convertResultToAttributesKvEntryList(input)
, readResultsProcessingExecutor);
} }
@Override @Override
@ -93,20 +124,19 @@ public class BaseAttributesDao extends AbstractDao implements AttributesDao {
} }
@Override @Override
public void removeAll(EntityId entityId, String attributeType, List<String> keys) { public ListenableFuture<List<ResultSet>> removeAll(EntityId entityId, String attributeType, List<String> keys) {
for (String key : keys) { List<ResultSetFuture> futures = keys.stream().map(key -> delete(entityId, attributeType, key)).collect(Collectors.toList());
delete(entityId, attributeType, key); return Futures.allAsList(futures);
}
} }
private void delete(EntityId entityId, String attributeType, String key) { private ResultSetFuture delete(EntityId entityId, String attributeType, String key) {
Statement delete = QueryBuilder.delete().all().from(ModelConstants.ATTRIBUTES_KV_CF) Statement delete = QueryBuilder.delete().all().from(ModelConstants.ATTRIBUTES_KV_CF)
.where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType())) .where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType()))
.and(eq(ENTITY_ID_COLUMN, entityId.getId())) .and(eq(ENTITY_ID_COLUMN, entityId.getId()))
.and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType)) .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType))
.and(eq(ATTRIBUTE_KEY_COLUMN, key)); .and(eq(ATTRIBUTE_KEY_COLUMN, key));
log.debug("Remove request: {}", delete.toString()); log.debug("Remove request: {}", delete.toString());
getSession().execute(delete); return getSession().executeAsync(delete);
} }
private PreparedStatement getSaveStmt() { private PreparedStatement getSaveStmt() {
@ -150,5 +180,4 @@ public class BaseAttributesDao extends AbstractDao implements AttributesDao {
} }
return entries; return entries;
} }
} }

19
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java

@ -27,7 +27,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -39,14 +41,21 @@ public class BaseAttributesService implements AttributesService {
private AttributesDao attributesDao; private AttributesDao attributesDao;
@Override @Override
public AttributeKvEntry find(EntityId entityId, String scope, String attributeKey) { public ListenableFuture<Optional<AttributeKvEntry>> find(EntityId entityId, String scope, String attributeKey) {
validate(entityId, scope); validate(entityId, scope);
Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey); Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey);
return attributesDao.find(entityId, scope, attributeKey); return attributesDao.find(entityId, scope, attributeKey);
} }
@Override @Override
public List<AttributeKvEntry> findAll(EntityId entityId, String scope) { public ListenableFuture<List<AttributeKvEntry>> find(EntityId entityId, String scope, Collection<String> attributeKeys) {
validate(entityId, scope);
attributeKeys.forEach(attributeKey -> Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey));
return attributesDao.find(entityId, scope, attributeKeys);
}
@Override
public ListenableFuture<List<AttributeKvEntry>> findAll(EntityId entityId, String scope) {
validate(entityId, scope); validate(entityId, scope);
return attributesDao.findAll(entityId, scope); return attributesDao.findAll(entityId, scope);
} }
@ -56,16 +65,16 @@ public class BaseAttributesService implements AttributesService {
validate(entityId, scope); validate(entityId, scope);
attributes.forEach(attribute -> validate(attribute)); attributes.forEach(attribute -> validate(attribute));
List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(attributes.size()); List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(attributes.size());
for(AttributeKvEntry attribute : attributes) { for (AttributeKvEntry attribute : attributes) {
futures.add(attributesDao.save(entityId, scope, attribute)); futures.add(attributesDao.save(entityId, scope, attribute));
} }
return Futures.allAsList(futures); return Futures.allAsList(futures);
} }
@Override @Override
public void removeAll(EntityId entityId, String scope, List<String> keys) { public ListenableFuture<List<ResultSet>> removeAll(EntityId entityId, String scope, List<String> keys) {
validate(entityId, scope); validate(entityId, scope);
attributesDao.removeAll(entityId, scope, keys); return attributesDao.removeAll(entityId, scope, keys);
} }
private static void validate(EntityId id, String scope) { private static void validate(EntityId id, String scope) {

22
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java

@ -19,6 +19,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.Dao;
@ -46,7 +47,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
* @return the list of device objects * @return the list of device objects
*/ */
List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink); List<DeviceEntity> findDevicesByTenantId(UUID tenantId, TextPageLink pageLink);
/**
* Find devices by tenantId and devices Ids.
*
* @param tenantId the tenantId
* @param deviceIds the device Ids
* @return the list of device objects
*/
ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds);
/** /**
* Find devices by tenantId, customerId and page link. * Find devices by tenantId, customerId and page link.
* *
@ -57,6 +67,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
*/ */
List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
/**
* Find devices by tenantId, customerId and devices Ids.
*
* @param tenantId the tenantId
* @param customerId the customerId
* @param deviceIds the device Ids
* @return the list of device objects
*/
ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds);
/** /**
* Find devices by tenantId and device name. * Find devices by tenantId and device name.
* *

23
dao/src/main/java/org/thingsboard/server/dao/device/DeviceDaoImpl.java

@ -16,12 +16,14 @@
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select; import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
import static org.thingsboard.server.dao.model.ModelConstants.*; import static org.thingsboard.server.dao.model.ModelConstants.*;
import java.util.*; import java.util.*;
import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -61,6 +63,16 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
return deviceEntities; return deviceEntities;
} }
@Override
public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdAndIdsAsync(UUID tenantId, List<UUID> deviceIds) {
log.debug("Try to find devices by tenantId [{}] and device Ids [{}]", tenantId, deviceIds);
Select select = select().from(getColumnFamilyName());
Select.Where query = select.where();
query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
query.and(in(ID_PROPERTY, deviceIds));
return findListByStatementAsync(query);
}
@Override @Override
public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) {
log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); log.debug("Try to find devices by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink);
@ -73,6 +85,17 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
return deviceEntities; return deviceEntities;
} }
@Override
public ListenableFuture<List<DeviceEntity>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds) {
log.debug("Try to find devices by tenantId [{}], customerId [{}] and device Ids [{}]", tenantId, customerId, deviceIds);
Select select = select().from(getColumnFamilyName());
Select.Where query = select.where();
query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
query.and(eq(DEVICE_CUSTOMER_ID_PROPERTY, customerId));
query.and(in(ID_PROPERTY, deviceIds));
return findListByStatementAsync(query);
}
@Override @Override
public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) { public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) {
Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME); Select select = select().from(DEVICE_BY_TENANT_AND_NAME_VIEW_NAME);

8
dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -22,12 +23,15 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface DeviceService { public interface DeviceService {
Device findDeviceById(DeviceId deviceId); Device findDeviceById(DeviceId deviceId);
ListenableFuture<Device> findDeviceByIdAsync(DeviceId deviceId);
Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name); Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name);
Device saveDevice(Device device); Device saveDevice(Device device);
@ -40,9 +44,13 @@ public interface DeviceService {
TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink); TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
void deleteDevicesByTenantId(TenantId tenantId); void deleteDevicesByTenantId(TenantId tenantId);
TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); TextPageData<Device> findDevicesByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds);
void unassignCustomerDevices(TenantId tenantId, CustomerId customerId); void unassignCustomerDevices(TenantId tenantId, CustomerId customerId);
} }

34
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -15,6 +15,9 @@
*/ */
package org.thingsboard.server.dao.device; package org.thingsboard.server.dao.device;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -42,8 +45,10 @@ import java.util.Optional;
import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.convertDataList;
import static org.thingsboard.server.dao.DaoUtil.getData; import static org.thingsboard.server.dao.DaoUtil.getData;
import static org.thingsboard.server.dao.DaoUtil.toUUIDs;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service @Service
@ -70,6 +75,14 @@ public class DeviceServiceImpl implements DeviceService {
return getData(deviceEntity); return getData(deviceEntity);
} }
@Override
public ListenableFuture<Device> findDeviceByIdAsync(DeviceId deviceId) {
log.trace("Executing findDeviceById [{}]", deviceId);
validateId(deviceId, "Incorrect deviceId " + deviceId);
ListenableFuture<DeviceEntity> deviceEntity = deviceDao.findByIdAsync(deviceId.getId());
return Futures.transform(deviceEntity, (Function<? super DeviceEntity, ? extends Device>) input -> getData(input));
}
@Override @Override
public Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name) { public Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findDeviceByTenantIdAndName [{}][{}]", tenantId, name); log.trace("Executing findDeviceByTenantIdAndName [{}][{}]", tenantId, name);
@ -132,6 +145,16 @@ public class DeviceServiceImpl implements DeviceService {
return new TextPageData<Device>(devices, pageLink); return new TextPageData<Device>(devices, pageLink);
} }
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdAndIdsAsync, tenantId [{}], deviceIds [{}]", tenantId, deviceIds);
validateId(tenantId, "Incorrect tenantId " + tenantId);
validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdAndIdsAsync(tenantId.getId(), toUUIDs(deviceIds));
return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
}
@Override @Override
public void deleteDevicesByTenantId(TenantId tenantId) { public void deleteDevicesByTenantId(TenantId tenantId) {
log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId); log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
@ -150,6 +173,17 @@ public class DeviceServiceImpl implements DeviceService {
return new TextPageData<Device>(devices, pageLink); return new TextPageData<Device>(devices, pageLink);
} }
@Override
public ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<DeviceId> deviceIds) {
log.trace("Executing findDevicesByTenantIdCustomerIdAndIdsAsync, tenantId [{}], customerId [{}], deviceIds [{}]", tenantId, customerId, deviceIds);
validateId(tenantId, "Incorrect tenantId " + tenantId);
validateId(customerId, "Incorrect customerId " + customerId);
validateIds(deviceIds, "Incorrect deviceIds " + deviceIds);
ListenableFuture<List<DeviceEntity>> deviceEntities = deviceDao.findDevicesByTenantIdCustomerIdAndIdsAsync(tenantId.getId(),
customerId.getId(), toUUIDs(deviceIds));
return Futures.transform(deviceEntities, (Function<List<DeviceEntity>, List<Device>>) input -> convertDataList(input));
}
@Override @Override
public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) { public void unassignCustomerDevices(TenantId tenantId, CustomerId customerId) {
log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId); log.trace("Executing unassignCustomerDevices, tenantId [{}], customerId [{}]", tenantId, customerId);

87
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -18,14 +18,16 @@ package org.thingsboard.server.dao.model;
import java.util.UUID; import java.util.UUID;
import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.core.utils.UUIDs;
import org.apache.commons.lang3.ArrayUtils;
import org.thingsboard.server.common.data.kv.Aggregation;
public class ModelConstants { public class ModelConstants {
private ModelConstants() { private ModelConstants() {
} }
public static UUID NULL_UUID = UUIDs.startOf(0); public static UUID NULL_UUID = UUIDs.startOf(0);
/** /**
* Generic constants. * Generic constants.
*/ */
@ -38,7 +40,7 @@ public class ModelConstants {
public static final String ALIAS_PROPERTY = "alias"; public static final String ALIAS_PROPERTY = "alias";
public static final String SEARCH_TEXT_PROPERTY = "search_text"; public static final String SEARCH_TEXT_PROPERTY = "search_text";
public static final String ADDITIONAL_INFO_PROPERTY = "additional_info"; public static final String ADDITIONAL_INFO_PROPERTY = "additional_info";
/** /**
* Cassandra user constants. * Cassandra user constants.
*/ */
@ -50,11 +52,11 @@ public class ModelConstants {
public static final String USER_FIRST_NAME_PROPERTY = "first_name"; public static final String USER_FIRST_NAME_PROPERTY = "first_name";
public static final String USER_LAST_NAME_PROPERTY = "last_name"; public static final String USER_LAST_NAME_PROPERTY = "last_name";
public static final String USER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String USER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String USER_BY_EMAIL_COLUMN_FAMILY_NAME = "user_by_email"; public static final String USER_BY_EMAIL_COLUMN_FAMILY_NAME = "user_by_email";
public static final String USER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "user_by_tenant_and_search_text"; public static final String USER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "user_by_tenant_and_search_text";
public static final String USER_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "user_by_customer_and_search_text"; public static final String USER_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "user_by_customer_and_search_text";
/** /**
* Cassandra user_credentials constants. * Cassandra user_credentials constants.
*/ */
@ -64,20 +66,20 @@ public class ModelConstants {
public static final String USER_CREDENTIALS_PASSWORD_PROPERTY = "password"; public static final String USER_CREDENTIALS_PASSWORD_PROPERTY = "password";
public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY = "activate_token"; public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY = "activate_token";
public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token"; public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token";
public static final String USER_CREDENTIALS_BY_USER_COLUMN_FAMILY_NAME = "user_credentials_by_user"; public static final String USER_CREDENTIALS_BY_USER_COLUMN_FAMILY_NAME = "user_credentials_by_user";
public static final String USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_activate_token"; public static final String USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_activate_token";
public static final String USER_CREDENTIALS_BY_RESET_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_reset_token"; public static final String USER_CREDENTIALS_BY_RESET_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_reset_token";
/** /**
* Cassandra admin_settings constants. * Cassandra admin_settings constants.
*/ */
public static final String ADMIN_SETTINGS_COLUMN_FAMILY_NAME = "admin_settings"; public static final String ADMIN_SETTINGS_COLUMN_FAMILY_NAME = "admin_settings";
public static final String ADMIN_SETTINGS_KEY_PROPERTY = "key"; public static final String ADMIN_SETTINGS_KEY_PROPERTY = "key";
public static final String ADMIN_SETTINGS_JSON_VALUE_PROPERTY = "json_value"; public static final String ADMIN_SETTINGS_JSON_VALUE_PROPERTY = "json_value";
public static final String ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME = "admin_settings_by_key"; public static final String ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME = "admin_settings_by_key";
/** /**
* Cassandra contact constants. * Cassandra contact constants.
*/ */
@ -97,9 +99,9 @@ public class ModelConstants {
public static final String TENANT_TITLE_PROPERTY = TITLE_PROPERTY; public static final String TENANT_TITLE_PROPERTY = TITLE_PROPERTY;
public static final String TENANT_REGION_PROPERTY = "region"; public static final String TENANT_REGION_PROPERTY = "region";
public static final String TENANT_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String TENANT_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "tenant_by_region_and_search_text"; public static final String TENANT_BY_REGION_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "tenant_by_region_and_search_text";
/** /**
* Cassandra customer constants. * Cassandra customer constants.
*/ */
@ -107,9 +109,9 @@ public class ModelConstants {
public static final String CUSTOMER_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY; public static final String CUSTOMER_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
public static final String CUSTOMER_TITLE_PROPERTY = TITLE_PROPERTY; public static final String CUSTOMER_TITLE_PROPERTY = TITLE_PROPERTY;
public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String CUSTOMER_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text"; public static final String CUSTOMER_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "customer_by_tenant_and_search_text";
/** /**
* Cassandra device constants. * Cassandra device constants.
*/ */
@ -118,12 +120,12 @@ public class ModelConstants {
public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String DEVICE_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY;
public static final String DEVICE_NAME_PROPERTY = "name"; public static final String DEVICE_NAME_PROPERTY = "name";
public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY; public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text"; public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text"; public static final String DEVICE_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_customer_and_search_text";
public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name";
/** /**
* Cassandra device_credentials constants. * Cassandra device_credentials constants.
*/ */
@ -132,7 +134,7 @@ public class ModelConstants {
public static final String DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY = "credentials_type"; public static final String DEVICE_CREDENTIALS_CREDENTIALS_TYPE_PROPERTY = "credentials_type";
public static final String DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY = "credentials_id"; public static final String DEVICE_CREDENTIALS_CREDENTIALS_ID_PROPERTY = "credentials_id";
public static final String DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY = "credentials_value"; public static final String DEVICE_CREDENTIALS_CREDENTIALS_VALUE_PROPERTY = "credentials_value";
public static final String DEVICE_CREDENTIALS_BY_DEVICE_COLUMN_FAMILY_NAME = "device_credentials_by_device"; public static final String DEVICE_CREDENTIALS_BY_DEVICE_COLUMN_FAMILY_NAME = "device_credentials_by_device";
public static final String DEVICE_CREDENTIALS_BY_CREDENTIALS_ID_COLUMN_FAMILY_NAME = "device_credentials_by_credentials_id"; public static final String DEVICE_CREDENTIALS_BY_CREDENTIALS_ID_COLUMN_FAMILY_NAME = "device_credentials_by_credentials_id";
@ -203,9 +205,9 @@ public class ModelConstants {
public static final String COMPONENT_DESCRIPTOR_BY_SCOPE_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "component_desc_by_scope_type_search_text"; public static final String COMPONENT_DESCRIPTOR_BY_SCOPE_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "component_desc_by_scope_type_search_text";
public static final String COMPONENT_DESCRIPTOR_BY_ID = "component_desc_by_id"; public static final String COMPONENT_DESCRIPTOR_BY_ID = "component_desc_by_id";
/** /**
* Cassandra rule metadata constants. * Cassandra rule metadata constants.
*/ */
public static final String RULE_COLUMN_FAMILY_NAME = "rule"; public static final String RULE_COLUMN_FAMILY_NAME = "rule";
public static final String RULE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY; public static final String RULE_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
public static final String RULE_NAME_PROPERTY = "name"; public static final String RULE_NAME_PROPERTY = "name";
@ -259,4 +261,51 @@ public class ModelConstants {
public static final String STRING_VALUE_COLUMN = "str_v"; public static final String STRING_VALUE_COLUMN = "str_v";
public static final String LONG_VALUE_COLUMN = "long_v"; public static final String LONG_VALUE_COLUMN = "long_v";
public static final String DOUBLE_VALUE_COLUMN = "dbl_v"; public static final String DOUBLE_VALUE_COLUMN = "dbl_v";
public static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
public static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN)};
public static final String[] MIN_AGGREGATION_COLUMNS = ArrayUtils.addAll(COUNT_AGGREGATION_COLUMNS,
new String[]{min(LONG_VALUE_COLUMN), min(DOUBLE_VALUE_COLUMN), min(BOOLEAN_VALUE_COLUMN), min(STRING_VALUE_COLUMN)});
public static final String[] MAX_AGGREGATION_COLUMNS = ArrayUtils.addAll(COUNT_AGGREGATION_COLUMNS,
new String[]{max(LONG_VALUE_COLUMN), max(DOUBLE_VALUE_COLUMN), max(BOOLEAN_VALUE_COLUMN), max(STRING_VALUE_COLUMN)});
public static final String[] SUM_AGGREGATION_COLUMNS = ArrayUtils.addAll(COUNT_AGGREGATION_COLUMNS,
new String[]{sum(LONG_VALUE_COLUMN), sum(DOUBLE_VALUE_COLUMN)});
public static final String[] AVG_AGGREGATION_COLUMNS = SUM_AGGREGATION_COLUMNS;
public static String min(String s) {
return "min(" + s + ")";
}
public static String max(String s) {
return "max(" + s + ")";
}
public static String sum(String s) {
return "sum(" + s + ")";
}
public static String count(String s) {
return "count(" + s + ")";
}
public static String[] getFetchColumnNames(Aggregation aggregation) {
switch (aggregation) {
case NONE:
return NONE_AGGREGATION_COLUMNS;
case MIN:
return MIN_AGGREGATION_COLUMNS;
case MAX:
return MAX_AGGREGATION_COLUMNS;
case SUM:
return SUM_AGGREGATION_COLUMNS;
case COUNT:
return COUNT_AGGREGATION_COLUMNS;
case AVG:
return AVG_AGGREGATION_COLUMNS;
default:
throw new RuntimeException("Aggregation type: " + aggregation + " is not supported!");
}
}
} }

18
dao/src/main/java/org/thingsboard/server/dao/service/Validator.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class Validator { public class Validator {
@ -77,6 +78,23 @@ public class Validator {
} }
} }
/**
* This method validate list of <code>UUIDBased</code> ids. If at least one of the ids is null than throw
* <code>IncorrectParameterException</code> exception
*
* @param ids the list of ids
* @param errorMessage the error message for exception
*/
public static void validateIds(List<? extends UUIDBased> ids, String errorMessage) {
if (ids == null || ids.isEmpty()) {
throw new IncorrectParameterException(errorMessage);
} else {
for (UUIDBased id : ids) {
validateId(id, errorMessage);
}
}
}
/** /**
* This method validate <code>PageLink</code> page link. If pageLink is invalid than throw * This method validate <code>PageLink</code> page link. If pageLink is invalid than throw
* <code>IncorrectParameterException</code> exception * <code>IncorrectParameterException</code> exception

193
dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java

@ -0,0 +1,193 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.timeseries;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import org.thingsboard.server.common.data.kv.*;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Optional;
/**
* Created by ashvayka on 20.02.17.
*/
public class AggregatePartitionsFunction implements com.google.common.base.Function<List<ResultSet>, Optional<TsKvEntry>> {
private static final int LONG_CNT_POS = 0;
private static final int DOUBLE_CNT_POS = 1;
private static final int BOOL_CNT_POS = 2;
private static final int STR_CNT_POS = 3;
private static final int LONG_POS = 4;
private static final int DOUBLE_POS = 5;
private static final int BOOL_POS = 6;
private static final int STR_POS = 7;
private final Aggregation aggregation;
private final String key;
private final long ts;
public AggregatePartitionsFunction(Aggregation aggregation, String key, long ts) {
this.aggregation = aggregation;
this.key = key;
this.ts = ts;
}
@Nullable
@Override
public Optional<TsKvEntry> apply(@Nullable List<ResultSet> rsList) {
if (rsList == null || rsList.size() == 0) {
return Optional.empty();
}
long count = 0;
DataType dataType = null;
Boolean bValue = null;
String sValue = null;
Double dValue = null;
Long lValue = null;
for (ResultSet rs : rsList) {
for (Row row : rs.all()) {
long curCount;
Long curLValue = null;
Double curDValue = null;
Boolean curBValue = null;
String curSValue = null;
long longCount = row.getLong(LONG_CNT_POS);
long doubleCount = row.getLong(DOUBLE_CNT_POS);
long boolCount = row.getLong(BOOL_CNT_POS);
long strCount = row.getLong(STR_CNT_POS);
if (longCount > 0) {
dataType = DataType.LONG;
curCount = longCount;
curLValue = getLongValue(row);
} else if (doubleCount > 0) {
dataType = DataType.DOUBLE;
curCount = doubleCount;
curDValue = getDoubleValue(row);
} else if (boolCount > 0) {
dataType = DataType.BOOLEAN;
curCount = boolCount;
curBValue = getBooleanValue(row);
} else if (strCount > 0) {
dataType = DataType.STRING;
curCount = strCount;
curSValue = getStringValue(row);
} else {
continue;
}
if (aggregation == Aggregation.COUNT) {
count += curCount;
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) {
count += curCount;
if (curDValue != null) {
dValue = dValue == null ? curDValue : dValue + curDValue;
} else if (curLValue != null) {
lValue = lValue == null ? curLValue : lValue + curLValue;
}
} else if (aggregation == Aggregation.MIN) {
if (curDValue != null) {
dValue = dValue == null ? curDValue : Math.min(dValue, curDValue);
} else if (curLValue != null) {
lValue = lValue == null ? curLValue : Math.min(lValue, curLValue);
} else if (curBValue != null) {
bValue = bValue == null ? curBValue : bValue && curBValue;
} else if (curSValue != null) {
if (sValue == null || curSValue.compareTo(sValue) < 0) {
sValue = curSValue;
}
}
} else if (aggregation == Aggregation.MAX) {
if (curDValue != null) {
dValue = dValue == null ? curDValue : Math.max(dValue, curDValue);
} else if (curLValue != null) {
lValue = lValue == null ? curLValue : Math.max(lValue, curLValue);
} else if (curBValue != null) {
bValue = bValue == null ? curBValue : bValue || curBValue;
} else if (curSValue != null) {
if (sValue == null || curSValue.compareTo(sValue) > 0) {
sValue = curSValue;
}
}
}
}
}
if (dataType == null) {
return Optional.empty();
} else if (aggregation == Aggregation.COUNT) {
return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, (long) count)));
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) {
if (count == 0 || (dataType == DataType.DOUBLE && dValue == null) || (dataType == DataType.LONG && lValue == null)) {
return Optional.empty();
} else if (dataType == DataType.DOUBLE) {
return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.SUM ? dValue : (dValue / count))));
} else if (dataType == DataType.LONG) {
return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggregation == Aggregation.SUM ? lValue : (lValue / count))));
}
} else if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) {
if (dataType == DataType.DOUBLE) {
return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, dValue)));
} else if (dataType == DataType.LONG) {
return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, lValue)));
} else if (dataType == DataType.STRING) {
return Optional.of(new BasicTsKvEntry(ts, new StringDataEntry(key, sValue)));
} else {
return Optional.of(new BasicTsKvEntry(ts, new BooleanDataEntry(key, bValue)));
}
}
return null;
}
private Boolean getBooleanValue(Row row) {
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) {
return row.getBool(BOOL_POS);
} else {
return null;
}
}
private String getStringValue(Row row) {
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) {
return row.getString(STR_POS);
} else {
return null;
}
}
private Long getLongValue(Row row) {
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX
|| aggregation == Aggregation.SUM || aggregation == Aggregation.AVG) {
return row.getLong(LONG_POS);
} else {
return null;
}
}
private Double getDoubleValue(Row row) {
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX
|| aggregation == Aggregation.SUM || aggregation == Aggregation.AVG) {
return row.getDouble(DOUBLE_POS);
} else {
return null;
}
}
}

247
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesDao.java

@ -18,15 +18,30 @@ package org.thingsboard.server.dao.timeseries;
import com.datastax.driver.core.*; import com.datastax.driver.core.*;
import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select; import com.datastax.driver.core.querybuilder.Select;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.dao.AbstractAsyncDao;
import org.thingsboard.server.dao.AbstractDao; import org.thingsboard.server.dao.AbstractDao;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.select; import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
@ -36,53 +51,186 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
*/ */
@Component @Component
@Slf4j @Slf4j
public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao { public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao {
@Value("${cassandra.query.max_limit_per_request}") //@Value("${cassandra.query.min_aggregation_step_ms}")
protected Integer maxLimitPerRequest; //TODO:
private int minAggregationStepMs = 1000;
@Value("${cassandra.query.ts_key_value_partitioning}")
private String partitioning;
private TsPartitionDate tsFormat;
private PreparedStatement partitionInsertStmt; private PreparedStatement partitionInsertStmt;
private PreparedStatement[] latestInsertStmts; private PreparedStatement[] latestInsertStmts;
private PreparedStatement[] saveStmts; private PreparedStatement[] saveStmts;
private PreparedStatement[] fetchStmts;
private PreparedStatement findLatestStmt; private PreparedStatement findLatestStmt;
private PreparedStatement findAllLatestStmt; private PreparedStatement findAllLatestStmt;
@PostConstruct
public void init() {
super.startExecutor();
getFetchStmt(Aggregation.NONE);
Optional<TsPartitionDate> partition = TsPartitionDate.parse(partitioning);
if (partition.isPresent()) {
tsFormat = partition.get();
} else {
log.warn("Incorrect configuration of partitioning {}", partitioning);
throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
}
}
@PreDestroy
public void stop() {
super.stopExecutor();
}
@Override
public long toPartitionTs(long ts) {
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC);
return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli();
}
@Override @Override
public List<TsKvEntry> find(String entityType, UUID entityId, TsKvQuery query, Optional<Long> minPartition, Optional<Long> maxPartition) { public ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, List<TsKvQuery> queries) {
List<Row> rows = Collections.emptyList(); List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(entityType, entityId, query)).collect(Collectors.toList());
Long[] parts = fetchPartitions(entityType, entityId, query.getKey(), minPartition, maxPartition); return Futures.transform(Futures.allAsList(futures), new Function<List<List<TsKvEntry>>, List<TsKvEntry>>() {
int partsLength = parts.length; @Nullable
if (parts != null && partsLength > 0) { @Override
int limit = maxLimitPerRequest; public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) {
Optional<Integer> lim = query.getLimit(); List<TsKvEntry> result = new ArrayList<TsKvEntry>();
if (lim.isPresent() && lim.get() < maxLimitPerRequest) { results.forEach(r -> result.addAll(r));
limit = lim.get(); return result;
} }
}, readResultsProcessingExecutor);
}
rows = new ArrayList<>(limit);
int lastIdx = partsLength - 1; private ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, TsKvQuery query) {
for (int i = 0; i < partsLength; i++) { if (query.getAggregation() == Aggregation.NONE) {
int currentLimit; return findAllAsyncWithLimit(entityType, entityId, query);
if (rows.size() >= limit) { } else {
break; long step = Math.max(query.getInterval(), minAggregationStepMs);
} else { long stepTs = query.getStartTs();
currentLimit = limit - rows.size(); List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
} while (stepTs < query.getEndTs()) {
Long partition = parts[i]; long startTs = stepTs;
Select.Where where = select().from(ModelConstants.TS_KV_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityType)) long endTs = stepTs + step;
.and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId)) TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation());
.and(eq(ModelConstants.KEY_COLUMN, query.getKey())) futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
.and(eq(ModelConstants.PARTITION_COLUMN, partition)); stepTs = endTs;
if (i == 0 && query.getStartTs().isPresent()) { }
where.and(QueryBuilder.gt(ModelConstants.TS_COLUMN, query.getStartTs().get())); ListenableFuture<List<Optional<TsKvEntry>>> future = Futures.allAsList(futures);
} else if (i == lastIdx && query.getEndTs().isPresent()) { return Futures.transform(future, new Function<List<Optional<TsKvEntry>>, List<TsKvEntry>>() {
where.and(QueryBuilder.lte(ModelConstants.TS_COLUMN, query.getEndTs().get())); @Nullable
@Override
public List<TsKvEntry> apply(@Nullable List<Optional<TsKvEntry>> input) {
return input.stream().filter(v -> v.isPresent()).map(v -> v.get()).collect(Collectors.toList());
} }
where.limit(currentLimit); }, readResultsProcessingExecutor);
rows.addAll(executeRead(where).all()); }
}
private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(String entityType, UUID entityId, TsKvQuery query) {
long minPartition = toPartitionTs(query.getStartTs());
long maxPartition = toPartitionTs(query.getEndTs());
ResultSetFuture partitionsFuture = fetchPartitions(entityType, entityId, query.getKey(), minPartition, maxPartition);
final SimpleListenableFuture<List<TsKvEntry>> resultFuture = new SimpleListenableFuture<>();
final ListenableFuture<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
@Override
public void onSuccess(@Nullable List<Long> partitions) {
TsKvQueryCursor cursor = new TsKvQueryCursor(entityType, entityId, query, partitions);
findAllAsyncSequentiallyWithLimit(cursor, resultFuture);
} }
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityType, entityId, minPartition, maxPartition, t);
}
}, readResultsProcessingExecutor);
return resultFuture;
}
private void findAllAsyncSequentiallyWithLimit(final TsKvQueryCursor cursor, final SimpleListenableFuture<List<TsKvEntry>> resultFuture) {
if (cursor.isFull() || !cursor.hasNextPartition()) {
resultFuture.set(cursor.getData());
} else {
PreparedStatement proto = getFetchStmt(Aggregation.NONE);
BoundStatement stmt = proto.bind();
stmt.setString(0, cursor.getEntityType());
stmt.setUUID(1, cursor.getEntityId());
stmt.setString(2, cursor.getKey());
stmt.setLong(3, cursor.getNextPartition());
stmt.setLong(4, cursor.getStartTs());
stmt.setLong(5, cursor.getEndTs());
stmt.setInt(6, cursor.getCurrentLimit());
Futures.addCallback(executeAsyncRead(stmt), new FutureCallback<ResultSet>() {
@Override
public void onSuccess(@Nullable ResultSet result) {
cursor.addData(convertResultToTsKvEntryList(result.all()));
findAllAsyncSequentiallyWithLimit(cursor, resultFuture);
}
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch data for query {}-{}", stmt, t);
}
}, readResultsProcessingExecutor);
} }
return convertResultToTsKvEntryList(rows); }
private ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(String entityType, UUID entityId, TsKvQuery query, long minPartition, long maxPartition) {
final Aggregation aggregation = query.getAggregation();
final String key = query.getKey();
final long startTs = query.getStartTs();
final long endTs = query.getEndTs();
final long ts = startTs + (endTs - startTs) / 2;
ResultSetFuture partitionsFuture = fetchPartitions(entityType, entityId, key, minPartition, maxPartition);
ListenableFuture<List<Long>> partitionsListFuture = Futures.transform(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
ListenableFuture<List<ResultSet>> aggregationChunks = Futures.transform(partitionsListFuture,
getFetchChunksAsyncFunction(entityType, entityId, key, aggregation, startTs, endTs), readResultsProcessingExecutor);
return Futures.transform(aggregationChunks, new AggregatePartitionsFunction(aggregation, key, ts), readResultsProcessingExecutor);
}
private Function<ResultSet, List<Long>> getPartitionsArrayFunction() {
return rows -> rows.all().stream()
.map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).collect(Collectors.toList());
}
private AsyncFunction<List<Long>, List<ResultSet>> getFetchChunksAsyncFunction(String entityType, UUID entityId, String key, Aggregation aggregation, long startTs, long endTs) {
return partitions -> {
try {
PreparedStatement proto = getFetchStmt(aggregation);
List<ResultSetFuture> futures = new ArrayList<>(partitions.size());
for (Long partition : partitions) {
BoundStatement stmt = proto.bind();
stmt.setString(0, entityType);
stmt.setUUID(1, entityId);
stmt.setString(2, key);
stmt.setLong(3, partition);
stmt.setLong(4, startTs);
stmt.setLong(5, endTs);
log.debug("Generated query [{}] for entityType {} and entityId {}", stmt, entityType, entityId);
futures.add(executeAsyncRead(stmt));
}
return Futures.allAsList(futures);
} catch (Throwable e) {
log.error("Failed to fetch data", e);
throw e;
}
};
} }
@Override @Override
@ -190,13 +338,12 @@ public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao {
* Select existing partitions from the table * Select existing partitions from the table
* <code>{@link ModelConstants#TS_KV_PARTITIONS_CF}</code> for the given entity * <code>{@link ModelConstants#TS_KV_PARTITIONS_CF}</code> for the given entity
*/ */
private Long[] fetchPartitions(String entityType, UUID entityId, String key, Optional<Long> minPartition, Optional<Long> maxPartition) { private ResultSetFuture fetchPartitions(String entityType, UUID entityId, String key, long minPartition, long maxPartition) {
Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityType)) Select.Where select = QueryBuilder.select(ModelConstants.PARTITION_COLUMN).from(ModelConstants.TS_KV_PARTITIONS_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityType))
.and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId)).and(eq(ModelConstants.KEY_COLUMN, key)); .and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId)).and(eq(ModelConstants.KEY_COLUMN, key));
minPartition.ifPresent(startTs -> select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition.get()))); select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
maxPartition.ifPresent(endTs -> select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition.get()))); select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
ResultSet resultSet = executeRead(select); return executeAsyncRead(select);
return resultSet.all().stream().map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).toArray(Long[]::new);
} }
private PreparedStatement getSaveStmt(DataType dataType) { private PreparedStatement getSaveStmt(DataType dataType) {
@ -216,6 +363,30 @@ public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao {
return saveStmts[dataType.ordinal()]; return saveStmts[dataType.ordinal()];
} }
private PreparedStatement getFetchStmt(Aggregation aggType) {
if (fetchStmts == null) {
fetchStmts = new PreparedStatement[Aggregation.values().length];
for (Aggregation type : Aggregation.values()) {
if (type == Aggregation.SUM && fetchStmts[Aggregation.AVG.ordinal()] != null) {
fetchStmts[type.ordinal()] = fetchStmts[Aggregation.AVG.ordinal()];
} else if (type == Aggregation.AVG && fetchStmts[Aggregation.SUM.ordinal()] != null) {
fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()];
} else {
fetchStmts[type.ordinal()] = getSession().prepare("SELECT " +
String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF
+ " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + " = ? "
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + " = ? "
+ "AND " + ModelConstants.KEY_COLUMN + " = ? "
+ "AND " + ModelConstants.PARTITION_COLUMN + " = ? "
+ "AND " + ModelConstants.TS_COLUMN + " > ? "
+ "AND " + ModelConstants.TS_COLUMN + " <= ?"
+ (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " DESC LIMIT ?" : ""));
}
}
}
return fetchStmts[aggType.ordinal()];
}
private PreparedStatement getLatestStmt(DataType dataType) { private PreparedStatement getLatestStmt(DataType dataType) {
if (latestInsertStmts == null) { if (latestInsertStmts == null) {
latestInsertStmts = new PreparedStatement[DataType.values().length]; latestInsertStmts = new PreparedStatement[DataType.values().length];

53
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -18,26 +18,31 @@ package org.thingsboard.server.dao.timeseries;
import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row; import com.datastax.driver.core.Row;
import com.google.common.base.Function;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.common.data.kv.TsKvQuery;
import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.Validator;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
@ -50,38 +55,14 @@ public class BaseTimeseriesService implements TimeseriesService {
public static final int INSERTS_PER_ENTRY = 3; public static final int INSERTS_PER_ENTRY = 3;
@Value("${cassandra.query.ts_key_value_partitioning}")
private String partitioning;
@Autowired @Autowired
private TimeseriesDao timeseriesDao; private TimeseriesDao timeseriesDao;
private TsPartitionDate tsFormat;
@PostConstruct
public void init() {
Optional<TsPartitionDate> partition = TsPartitionDate.parse(partitioning);
if (partition.isPresent()) {
tsFormat = partition.get();
} else {
log.warn("Incorrect configuration of partitioning {}", partitioning);
throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
}
}
@Override @Override
public List<TsKvEntry> find(String entityType, UUIDBased entityId, TsKvQuery query) { public ListenableFuture<List<TsKvEntry>> findAll(String entityType, UUIDBased entityId, List<TsKvQuery> queries) {
validate(entityType, entityId); validate(entityType, entityId);
validate(query); queries.forEach(query -> validate(query));
return timeseriesDao.find(entityType, entityId.getId(), query, toPartitionTs(query.getStartTs()), toPartitionTs(query.getEndTs())); return timeseriesDao.findAllAsync(entityType, entityId.getId(), queries);
}
private Optional<Long> toPartitionTs(Optional<Long> ts) {
if (ts.isPresent()) {
return Optional.of(toPartitionTs(ts.get()));
} else {
return Optional.empty();
}
} }
@Override @Override
@ -106,7 +87,7 @@ public class BaseTimeseriesService implements TimeseriesService {
throw new IncorrectParameterException("Key value entry can't be null"); throw new IncorrectParameterException("Key value entry can't be null");
} }
UUID uid = entityId.getId(); UUID uid = entityId.getId();
long partitionTs = toPartitionTs(tsKvEntry.getTs()); long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs); saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs);
@ -122,7 +103,7 @@ public class BaseTimeseriesService implements TimeseriesService {
throw new IncorrectParameterException("Key value entry can't be null"); throw new IncorrectParameterException("Key value entry can't be null");
} }
UUID uid = entityId.getId(); UUID uid = entityId.getId();
long partitionTs = toPartitionTs(tsKvEntry.getTs()); long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs); saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs);
} }
return Futures.allAsList(futures); return Futures.allAsList(futures);
@ -144,14 +125,6 @@ public class BaseTimeseriesService implements TimeseriesService {
futures.add(timeseriesDao.save(entityType, uid, partitionTs, tsKvEntry)); futures.add(timeseriesDao.save(entityType, uid, partitionTs, tsKvEntry));
} }
private long toPartitionTs(long ts) {
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC);
LocalDateTime parititonTime = tsFormat.truncatedTo(time);
return parititonTime.toInstant(ZoneOffset.UTC).toEpochMilli();
}
private static void validate(String entityType, UUIDBased entityId) { private static void validate(String entityType, UUIDBased entityId) {
Validator.validateString(entityType, "Incorrect entityType " + entityType); Validator.validateString(entityType, "Incorrect entityType " + entityType);
Validator.validateId(entityId, "Incorrect entityId " + entityId); Validator.validateId(entityId, "Incorrect entityId " + entityId);
@ -162,6 +135,8 @@ public class BaseTimeseriesService implements TimeseriesService {
throw new IncorrectParameterException("TsKvQuery can't be null"); throw new IncorrectParameterException("TsKvQuery can't be null");
} else if (isBlank(query.getKey())) { } else if (isBlank(query.getKey())) {
throw new IncorrectParameterException("Incorrect TsKvQuery. Key can't be empty"); throw new IncorrectParameterException("Incorrect TsKvQuery. Key can't be empty");
} else if (query.getAggregation() == null) {
throw new IncorrectParameterException("Incorrect TsKvQuery. Aggregation can't be empty");
} }
} }
} }

29
dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.timeseries;
import com.google.common.util.concurrent.AbstractFuture;
/**
* Created by ashvayka on 21.02.17.
*/
public class SimpleListenableFuture<V> extends AbstractFuture<V> {
public boolean set(V value) {
return super.set(value);
}
}

5
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java

@ -17,6 +17,7 @@ package org.thingsboard.server.dao.timeseries;
import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row; import com.datastax.driver.core.Row;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.common.data.kv.TsKvQuery;
@ -30,7 +31,9 @@ import java.util.UUID;
*/ */
public interface TimeseriesDao { public interface TimeseriesDao {
List<TsKvEntry> find(String entityType, UUID entityId, TsKvQuery query, Optional<Long> minPartition, Optional<Long> maxPartition); long toPartitionTs(long ts);
ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, List<TsKvQuery> queries);
ResultSetFuture findLatest(String entityType, UUID entityId, String key); ResultSetFuture findLatest(String entityType, UUID entityId, String key);

4
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -19,6 +19,7 @@ import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row; import com.datastax.driver.core.Row;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.common.data.kv.TsKvQuery;
@ -32,8 +33,7 @@ import java.util.Set;
*/ */
public interface TimeseriesService { public interface TimeseriesService {
//TODO: Replace this with async operation ListenableFuture<List<TsKvEntry>> findAll(String entityType, UUIDBased entityId, List<TsKvQuery> queries);
List<TsKvEntry> find(String entityType, UUIDBased entityId, TsKvQuery query);
ListenableFuture<List<ResultSet>> findLatest(String entityType, UUIDBased entityId, Collection<String> keys); ListenableFuture<List<ResultSet>> findLatest(String entityType, UUIDBased entityId, Collection<String> keys);

82
dao/src/main/java/org/thingsboard/server/dao/timeseries/TsKvQueryCursor.java

@ -0,0 +1,82 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.timeseries;
import lombok.Data;
import lombok.Getter;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Created by ashvayka on 21.02.17.
*/
public class TsKvQueryCursor {
@Getter
private final String entityType;
@Getter
private final UUID entityId;
@Getter
private final String key;
@Getter
private final long startTs;
@Getter
private final long endTs;
private final List<Long> partitions;
@Getter
private final List<TsKvEntry> data;
private int partitionIndex;
private int currentLimit;
public TsKvQueryCursor(String entityType, UUID entityId, TsKvQuery baseQuery, List<Long> partitions) {
this.entityType = entityType;
this.entityId = entityId;
this.key = baseQuery.getKey();
this.startTs = baseQuery.getStartTs();
this.endTs = baseQuery.getEndTs();
this.partitions = partitions;
this.partitionIndex = partitions.size() - 1;
this.data = new ArrayList<>();
this.currentLimit = baseQuery.getLimit();
}
public boolean hasNextPartition() {
return partitionIndex >= 0;
}
public boolean isFull() {
return currentLimit <= 0;
}
public long getNextPartition() {
long partition = partitions.get(partitionIndex);
partitionIndex--;
return partition;
}
public int getCurrentLimit() {
return currentLimit;
}
public void addData(List<TsKvEntry> newData) {
currentLimit -= newData.size();
data.addAll(newData);
}
}

240
dao/src/main/resources/system-data.cql

File diff suppressed because one or more lines are too long

18
dao/src/test/java/org/thingsboard/server/dao/attributes/BaseAttributesServiceTest.java

@ -32,6 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
import static org.thingsboard.server.common.data.DataConstants.DEVICE; import static org.thingsboard.server.common.data.DataConstants.DEVICE;
@ -54,8 +55,9 @@ public class BaseAttributesServiceTest extends AbstractServiceTest {
KvEntry attrValue = new StringDataEntry("attribute1", "value1"); KvEntry attrValue = new StringDataEntry("attribute1", "value1");
AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L); AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L);
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attr)).get(); attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attr)).get();
AttributeKvEntry saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attr.getKey()); Optional<AttributeKvEntry> saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attr.getKey()).get();
Assert.assertEquals(attr, saved); Assert.assertTrue(saved.isPresent());
Assert.assertEquals(attr, saved.get());
} }
@Test @Test
@ -65,15 +67,17 @@ public class BaseAttributesServiceTest extends AbstractServiceTest {
AttributeKvEntry attrOld = new BaseAttributeKvEntry(attrOldValue, 42L); AttributeKvEntry attrOld = new BaseAttributeKvEntry(attrOldValue, 42L);
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrOld)).get(); attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrOld)).get();
AttributeKvEntry saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey()); Optional<AttributeKvEntry> saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey()).get();
Assert.assertEquals(attrOld, saved);
Assert.assertTrue(saved.isPresent());
Assert.assertEquals(attrOld, saved.get());
KvEntry attrNewValue = new StringDataEntry("attribute1", "value2"); KvEntry attrNewValue = new StringDataEntry("attribute1", "value2");
AttributeKvEntry attrNew = new BaseAttributeKvEntry(attrNewValue, 73L); AttributeKvEntry attrNew = new BaseAttributeKvEntry(attrNewValue, 73L);
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrNew)).get(); attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrNew)).get();
saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey()); saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey()).get();
Assert.assertEquals(attrNew, saved); Assert.assertEquals(attrNew, saved.get());
} }
@Test @Test
@ -91,7 +95,7 @@ public class BaseAttributesServiceTest extends AbstractServiceTest {
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrANew)).get(); attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrANew)).get();
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrBNew)).get(); attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrBNew)).get();
List<AttributeKvEntry> saved = attributesService.findAll(deviceId, DataConstants.CLIENT_SCOPE); List<AttributeKvEntry> saved = attributesService.findAll(deviceId, DataConstants.CLIENT_SCOPE).get();
Assert.assertNotNull(saved); Assert.assertNotNull(saved);
Assert.assertEquals(2, saved.size()); Assert.assertEquals(2, saved.size());

112
dao/src/test/java/org/thingsboard/server/dao/timeseries/TimeseriesServiceTest.java

@ -51,8 +51,6 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
private static final String DOUBLE_KEY = "doubleKey"; private static final String DOUBLE_KEY = "doubleKey";
private static final String BOOLEAN_KEY = "booleanKey"; private static final String BOOLEAN_KEY = "booleanKey";
public static final int PARTITION_MINUTES = 1100;
private static final long TS = 42L; private static final long TS = 42L;
KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value"); KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value");
@ -103,27 +101,101 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
} }
@Test @Test
public void testFindDeviceTsDataByQuery() throws Exception { public void testFindDeviceTsData() throws Exception {
DeviceId deviceId = new DeviceId(UUIDs.timeBased()); DeviceId deviceId = new DeviceId(UUIDs.timeBased());
LocalDateTime localDateTime = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(PARTITION_MINUTES); List<TsKvEntry> entries = new ArrayList<>();
log.debug("Start event time is {}", localDateTime);
List<TsKvEntry> entries = new ArrayList<>(PARTITION_MINUTES); entries.add(save(deviceId, 5000, 100));
entries.add(save(deviceId, 15000, 200));
for (int i = 0; i < PARTITION_MINUTES; i++) {
long time = localDateTime.plusMinutes(i).toInstant(ZoneOffset.UTC).toEpochMilli(); entries.add(save(deviceId, 25000, 300));
BasicTsKvEntry tsKvEntry = new BasicTsKvEntry(time, stringKvEntry); entries.add(save(deviceId, 35000, 400));
tsService.save(DataConstants.DEVICE, deviceId, tsKvEntry).get();
entries.add(tsKvEntry); entries.add(save(deviceId, 45000, 500));
} entries.add(save(deviceId, 55000, 600));
log.debug("Saved all records {}", localDateTime);
List<TsKvEntry> list = tsService.find(DataConstants.DEVICE, deviceId, new BaseTsKvQuery(STRING_KEY, entries.get(599).getTs(), List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli())); 60000, 20000, 3, Aggregation.NONE))).get();
log.debug("Fetched records {}", localDateTime); assertEquals(3, list.size());
List<TsKvEntry> expected = entries.subList(600, PARTITION_MINUTES); assertEquals(55000, list.get(0).getTs());
assertEquals(expected.size(), list.size()); assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue());
assertEquals(expected, list);
assertEquals(45000, list.get(1).getTs());
assertEquals(java.util.Optional.of(500L), list.get(1).getLongValue());
assertEquals(35000, list.get(2).getTs());
assertEquals(java.util.Optional.of(400L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.AVG))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
assertEquals(java.util.Optional.of(150L), list.get(0).getLongValue());
assertEquals(30000, list.get(1).getTs());
assertEquals(java.util.Optional.of(350L), list.get(1).getLongValue());
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(550L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.SUM))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
assertEquals(java.util.Optional.of(300L), list.get(0).getLongValue());
assertEquals(30000, list.get(1).getTs());
assertEquals(java.util.Optional.of(700L), list.get(1).getLongValue());
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(1100L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.MIN))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
assertEquals(java.util.Optional.of(100L), list.get(0).getLongValue());
assertEquals(30000, list.get(1).getTs());
assertEquals(java.util.Optional.of(300L), list.get(1).getLongValue());
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(500L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.MAX))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
assertEquals(java.util.Optional.of(200L), list.get(0).getLongValue());
assertEquals(30000, list.get(1).getTs());
assertEquals(java.util.Optional.of(400L), list.get(1).getLongValue());
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(600L), list.get(2).getLongValue());
list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.COUNT))).get();
assertEquals(3, list.size());
assertEquals(10000, list.get(0).getTs());
assertEquals(java.util.Optional.of(2L), list.get(0).getLongValue());
assertEquals(30000, list.get(1).getTs());
assertEquals(java.util.Optional.of(2L), list.get(1).getLongValue());
assertEquals(50000, list.get(2).getTs());
assertEquals(java.util.Optional.of(2L), list.get(2).getLongValue());
} }
private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception {
TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value));
tsService.save(DataConstants.DEVICE, deviceId, entry).get();
return entry;
}
private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException { private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException {
tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, stringKvEntry)).get(); tsService.save(DataConstants.DEVICE, deviceId, toTsEntry(ts, stringKvEntry)).get();

2
dao/src/test/resources/cassandra-test.properties

@ -47,3 +47,5 @@ cassandra.query.default_fetch_size=2000
cassandra.query.ts_key_value_partitioning=HOURS cassandra.query.ts_key_value_partitioning=HOURS
cassandra.query.max_limit_per_request=1000 cassandra.query.max_limit_per_request=1000
cassandra.query.min_aggregation_step_ms=100

4
docker/docker-compose.yml

@ -18,7 +18,7 @@ version: '2'
services: services:
thingsboard: thingsboard:
image: "thingsboard/application:1.1.0" image: "thingsboard/application:1.2.0"
ports: ports:
- "8080:8080" - "8080:8080"
- "1883:1883" - "1883:1883"
@ -27,7 +27,7 @@ services:
- thingsboard.env - thingsboard.env
entrypoint: ./run_thingsboard.sh entrypoint: ./run_thingsboard.sh
thingsboard-db-schema: thingsboard-db-schema:
image: "thingsboard/thingsboard-db-schema:1.1.0" image: "thingsboard/thingsboard-db-schema:1.2.0"
env_file: env_file:
- thingsboard-db-schema.env - thingsboard-db-schema.env
entrypoint: ./install_schema.sh entrypoint: ./install_schema.sh

4
docker/thingsboard-db-schema/build_and_deploy.sh

@ -20,9 +20,9 @@ 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/demo-data.cql demo-data.cql
cp ../../dao/src/main/resources/system-data.cql system-data.cql cp ../../dao/src/main/resources/system-data.cql system-data.cql
docker build -t thingsboard/thingsboard-db-schema:1.1.0 -t thingsboard/thingsboard-db-schema:latest . docker build -t thingsboard/thingsboard-db-schema:1.2.0 -t thingsboard/thingsboard-db-schema:latest .
docker login docker login
docker push thingsboard/thingsboard-db-schema:1.1.0 docker push thingsboard/thingsboard-db-schema:1.2.0
docker push thingsboard/thingsboard-db-schema:latest docker push thingsboard/thingsboard-db-schema:latest

4
docker/thingsboard/build_and_deploy.sh

@ -18,9 +18,9 @@
cp ../../application/target/thingsboard.deb thingsboard.deb cp ../../application/target/thingsboard.deb thingsboard.deb
docker build -t thingsboard/application:1.1.0 -t thingsboard/application:latest . docker build -t thingsboard/application:1.2.0 -t thingsboard/application:latest .
docker login docker login
docker push thingsboard/application:1.1.0 docker push thingsboard/application:1.2.0
docker push thingsboard/application:latest docker push thingsboard/application:latest

2
extensions-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

26
extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.extensions.api.device; package org.thingsboard.server.extensions.api.device;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import java.util.*; import java.util.*;
@ -65,4 +67,28 @@ public class DeviceAttributes {
public Optional<AttributeKvEntry> getServerPublicAttribute(String attribute) { public Optional<AttributeKvEntry> getServerPublicAttribute(String attribute) {
return Optional.ofNullable(serverPublicAttributesMap.get(attribute)); return Optional.ofNullable(serverPublicAttributesMap.get(attribute));
} }
public void remove(AttributeKey key) {
Map<String, AttributeKvEntry> map = getMapByScope(key.getScope());
if (map != null) {
map.remove(key);
}
}
public void update(String scope, List<AttributeKvEntry> values) {
Map<String, AttributeKvEntry> map = getMapByScope(scope);
values.forEach(v -> map.put(v.getKey(), v));
}
private Map<String, AttributeKvEntry> getMapByScope(String scope) {
Map<String, AttributeKvEntry> map = null;
if (scope.equalsIgnoreCase(DataConstants.CLIENT_SCOPE)) {
map = clientSideAttributesMap;
} else if (scope.equalsIgnoreCase(DataConstants.SHARED_SCOPE)) {
map = serverPublicAttributesMap;
} else if (scope.equalsIgnoreCase(DataConstants.SERVER_SCOPE)) {
map = serverPrivateAttributesMap;
}
return map;
}
} }

32
extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java

@ -15,37 +15,43 @@
*/ */
package org.thingsboard.server.extensions.api.device; package org.thingsboard.server.extensions.api.device;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@ToString @ToString
@AllArgsConstructor
public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg { public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg {
@Getter private final TenantId tenantId; @Getter
@Getter private final DeviceId deviceId; private final TenantId tenantId;
@Getter private final Set<AttributeKey> keys; @Getter
@Getter private final boolean deleted; private final DeviceId deviceId;
@Getter
private final Set<AttributeKey> deletedKeys;
@Getter
private final String scope;
@Getter
private final List<AttributeKvEntry> values;
@Getter
private final boolean deleted;
public static DeviceAttributesEventNotificationMsg onUpdate(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) { public static DeviceAttributesEventNotificationMsg onUpdate(TenantId tenantId, DeviceId deviceId, String scope, List<AttributeKvEntry> values) {
return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, false); return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, null, scope, values, false);
} }
public static DeviceAttributesEventNotificationMsg onDelete(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) { public static DeviceAttributesEventNotificationMsg onDelete(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) {
return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, true); return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, null, null, true);
} }
private DeviceAttributesEventNotificationMsg(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys, boolean deleted) {
this.tenantId = tenantId;
this.deviceId = deviceId;
this.keys = keys;
this.deleted = deleted;
}
} }

22
extensions-api/src/main/java/org/thingsboard/server/extensions/api/exception/UnauthorizedException.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.api.exception;
/**
* Created by ashvayka on 21.02.17.
*/
public class UnauthorizedException extends Exception {
}

18
extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java

@ -42,7 +42,7 @@ public interface PluginContext {
void reply(PluginToRuleMsg<?> msg); void reply(PluginToRuleMsg<?> msg);
boolean checkAccess(DeviceId deviceId); void checkAccess(DeviceId deviceId, PluginCallback<Void> callback);
Optional<PluginApiCallSecurityContext> getSecurityCtx(); Optional<PluginApiCallSecurityContext> getSecurityCtx();
@ -82,7 +82,7 @@ public interface PluginContext {
void saveTsData(DeviceId deviceId, List<TsKvEntry> entry, PluginCallback<Void> callback); void saveTsData(DeviceId deviceId, List<TsKvEntry> entry, PluginCallback<Void> callback);
List<TsKvEntry> loadTimeseries(DeviceId deviceId, TsKvQuery query); void loadTimeseries(DeviceId deviceId, List<TsKvQuery> queries, PluginCallback<List<TsKvEntry>> callback);
void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback); void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback);
@ -92,15 +92,19 @@ public interface PluginContext {
Attributes API Attributes API
*/ */
void saveAttributes(DeviceId deviceId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback); void saveAttributes(TenantId tenantId, DeviceId deviceId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
Optional<AttributeKvEntry> loadAttribute(DeviceId deviceId, String attributeType, String attributeKey); void removeAttributes(TenantId tenantId, DeviceId deviceId, String scope, List<String> attributeKeys, PluginCallback<Void> callback);
List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType, List<String> attributeKeys); void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, PluginCallback<Optional<AttributeKvEntry>> callback);
List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType); void loadAttributes(DeviceId deviceId, String attributeType, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
void removeAttributes(DeviceId deviceId, String scope, List<String> attributeKeys); void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback);
void loadAttributes(DeviceId deviceId, Collection<String> attributeTypes, PluginCallback<List<AttributeKvEntry>> callback);
void loadAttributes(DeviceId deviceId, Collection<String> attributeTypes, Collection<String> attributeKeys, PluginCallback<List<AttributeKvEntry>> callback);
void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback); void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback);
} }

2
extensions-core/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

42
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java

@ -25,6 +25,7 @@ import org.springframework.util.StringUtils;
import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext; import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler; import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler;
import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse; import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
@ -62,27 +63,34 @@ public class RpcRestMsgHandler extends DefaultRestMsgHandler {
String method = pathParams[0].toUpperCase(); String method = pathParams[0].toUpperCase();
if (DataConstants.ONEWAY.equals(method) || DataConstants.TWOWAY.equals(method)) { if (DataConstants.ONEWAY.equals(method) || DataConstants.TWOWAY.equals(method)) {
DeviceId deviceId = DeviceId.fromString(pathParams[1]); DeviceId deviceId = DeviceId.fromString(pathParams[1]);
if (!ctx.checkAccess(deviceId)) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
return;
}
JsonNode rpcRequestBody = jsonMapper.readTree(request.getRequestBody()); JsonNode rpcRequestBody = jsonMapper.readTree(request.getRequestBody());
RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(), RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(),
jsonMapper.writeValueAsString(rpcRequestBody.get("params"))); jsonMapper.writeValueAsString(rpcRequestBody.get("params")));
if (rpcRequestBody.has("timeout")) {
cmd.setTimeout(rpcRequestBody.get("timeout").asLong()); ctx.checkAccess(deviceId, new PluginCallback<Void>() {
} @Override
long timeout = cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout; public void onSuccess(PluginContext ctx, Void value) {
ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData()); if (rpcRequestBody.has("timeout")) {
ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(), cmd.setTimeout(rpcRequestBody.get("timeout").asLong());
ctx.getSecurityCtx().orElseThrow(() -> new IllegalStateException("Security context is empty!")).getTenantId(), }
deviceId, long timeout = cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout;
DataConstants.ONEWAY.equals(method), ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
System.currentTimeMillis() + timeout, ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
body ctx.getSecurityCtx().orElseThrow(() -> new IllegalStateException("Security context is empty!")).getTenantId(),
); deviceId,
rpcManager.process(ctx, new LocalRequestMetaData(rpcRequest, msg.getResponseHolder())); DataConstants.ONEWAY.equals(method),
System.currentTimeMillis() + timeout,
body
);
rpcManager.process(ctx, new LocalRequestMetaData(rpcRequest, msg.getResponseHolder()));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
}
});
valid = true; valid = true;
} }
} }

51
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java

@ -15,12 +15,14 @@
*/ */
package org.thingsboard.server.extensions.core.plugin.telemetry; package org.thingsboard.server.extensions.core.plugin.telemetry;
import com.sun.javafx.collections.MappingChange;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext; import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler; import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler;
import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler; import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler;
@ -66,28 +68,49 @@ public class SubscriptionManager {
DeviceId deviceId = subscription.getDeviceId(); DeviceId deviceId = subscription.getDeviceId();
log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), deviceId, address); log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), deviceId, address);
registerSubscription(sessionId, deviceId, subscription); registerSubscription(sessionId, deviceId, subscription);
List<TsKvEntry> missedUpdates = new ArrayList<>();
if (subscription.getType() == SubscriptionType.ATTRIBUTES) { if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
subscription.getKeyStates().entrySet().forEach(e -> { final Map<String, Long> keyStates = subscription.getKeyStates();
Optional<AttributeKvEntry> latestOpt = ctx.loadAttribute(deviceId, DataConstants.CLIENT_SCOPE, e.getKey()); ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
if (latestOpt.isPresent()) { @Override
AttributeKvEntry latestEntry = latestOpt.get(); public void onSuccess(PluginContext ctx, List<AttributeKvEntry> values) {
if (latestEntry.getLastUpdateTs() > e.getValue()) { List<TsKvEntry> missedUpdates = new ArrayList<>();
missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry)); values.forEach(latestEntry -> {
} if (latestEntry.getLastUpdateTs() > keyStates.get(latestEntry.getKey())) {
missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry));
} }
});
if (!missedUpdates.isEmpty()) {
rpcHandler.onSubscriptionUpdate(ctx, address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
} }
); }
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch missed updates.", e);
}
});
} else if (subscription.getType() == SubscriptionType.TIMESERIES) { } else if (subscription.getType() == SubscriptionType.TIMESERIES) {
long curTs = System.currentTimeMillis(); long curTs = System.currentTimeMillis();
List<TsKvQuery> queries = new ArrayList<>();
subscription.getKeyStates().entrySet().forEach(e -> { subscription.getKeyStates().entrySet().forEach(e -> {
TsKvQuery query = new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs); queries.add(new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs));
missedUpdates.addAll(ctx.loadTimeseries(deviceId, query)); });
ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> missedUpdates) {
if (!missedUpdates.isEmpty()) {
rpcHandler.onSubscriptionUpdate(ctx, address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
}
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch missed updates.", e);
}
}); });
} }
if (!missedUpdates.isEmpty()) {
rpcHandler.onSubscriptionUpdate(ctx, address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
}
} }
private void registerSubscription(String sessionId, DeviceId deviceId, Subscription subscription) { private void registerSubscription(String sessionId, DeviceId deviceId, Subscription subscription) {

51
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java

@ -15,9 +15,16 @@
*/ */
package org.thingsboard.server.extensions.core.plugin.telemetry.cmd; package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GetHistoryCmd implements TelemetryPluginCmd { public class GetHistoryCmd implements TelemetryPluginCmd {
private int cmdId; private int cmdId;
@ -25,46 +32,8 @@ public class GetHistoryCmd implements TelemetryPluginCmd {
private String keys; private String keys;
private long startTs; private long startTs;
private long endTs; private long endTs;
private long interval;
private int limit;
private String agg;
@Override
public int getCmdId() {
return cmdId;
}
@Override
public void setCmdId(int cmdId) {
this.cmdId = cmdId;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getKeys() {
return keys;
}
public void setKeys(String keys) {
this.keys = keys;
}
public long getStartTs() {
return startTs;
}
public void setStartTs(long startTs) {
this.startTs = startTs;
}
public long getEndTs() {
return endTs;
}
public void setEndTs(long endTs) {
this.endTs = endTs;
}
} }

42
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java

@ -16,11 +16,13 @@
package org.thingsboard.server.extensions.core.plugin.telemetry.cmd; package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType; import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Data
public abstract class SubscriptionCmd implements TelemetryPluginCmd { public abstract class SubscriptionCmd implements TelemetryPluginCmd {
private int cmdId; private int cmdId;
@ -31,46 +33,6 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
public abstract SubscriptionType getType(); public abstract SubscriptionType getType();
public int getCmdId() {
return cmdId;
}
public void setCmdId(int cmdId) {
this.cmdId = cmdId;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getKeys() {
return keys;
}
public void setTags(String tags) {
this.keys = tags;
}
public boolean isUnsubscribe() {
return unsubscribe;
}
public void setUnsubscribe(boolean unsubscribe) {
this.unsubscribe = unsubscribe;
}
public String getScope() {
return scope;
}
public void setKeys(String keys) {
this.keys = keys;
}
@Override @Override
public String toString() { public String toString() {
return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]"; return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";

16
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.extensions.core.plugin.telemetry.cmd; package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType; import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
@ -22,17 +24,15 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
@Data
public class TimeseriesSubscriptionCmd extends SubscriptionCmd { public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private long startTs;
private long timeWindow; private long timeWindow;
private long interval;
public long getTimeWindow() { private int limit;
return timeWindow; private String agg;
}
public void setTimeWindow(long timeWindow) {
this.timeWindow = timeWindow;
}
@Override @Override
public SubscriptionType getType() { public SubscriptionType getType() {

74
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/BiPluginCallBack.java

@ -0,0 +1,74 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext;
/**
* Created by ashvayka on 21.02.17.
*/
@Slf4j
public abstract class BiPluginCallBack<V1, V2> {
private V1 v1;
private V2 v2;
public PluginCallback<V1> getV1Callback() {
return new PluginCallback<V1>() {
@Override
public void onSuccess(PluginContext ctx, V1 value) {
synchronized (BiPluginCallBack.this) {
BiPluginCallBack.this.v1 = value;
if (v2 != null) {
BiPluginCallBack.this.onSuccess(ctx, v1, v2);
}
}
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
BiPluginCallBack.this.onFailure(ctx, e);
}
};
}
public PluginCallback<V2> getV2Callback() {
return new PluginCallback<V2>() {
@Override
public void onSuccess(PluginContext ctx, V2 value) {
synchronized (BiPluginCallBack.this) {
BiPluginCallBack.this.v2 = value;
if (v1 != null) {
BiPluginCallBack.this.onSuccess(ctx, v1, v2);
}
}
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
BiPluginCallBack.this.onFailure(ctx, e);
}
};
}
abstract public void onSuccess(PluginContext ctx, V1 v1, V2 v2);
abstract public void onFailure(PluginContext ctx, Exception e);
}

121
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java

@ -77,40 +77,59 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
} }
}); });
} else if (entity.equals("attributes")) { } else if (entity.equals("attributes")) {
List<AttributeKvEntry> attributes; PluginCallback<List<AttributeKvEntry>> callback = getAttributeKeysPluginCallback(msg);
if (!StringUtils.isEmpty(scope)) { if (!StringUtils.isEmpty(scope)) {
attributes = ctx.loadAttributes(deviceId, scope); ctx.loadAttributes(deviceId, scope, callback);
} else { } else {
attributes = new ArrayList<>(); ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(ctx.loadAttributes(deviceId, s)));
} }
List<String> keys = attributes.stream().map(attrKv -> attrKv.getKey()).collect(Collectors.toList());
msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
} }
} else if (method.equals("values")) { } else if (method.equals("values")) {
if ("timeseries".equals(entity)) { if ("timeseries".equals(entity)) {
String keys = request.getParameter("keys"); String keysStr = request.getParameter("keys");
Optional<Long> startTs = request.getLongParamValue("startTs"); Optional<Long> startTs = request.getLongParamValue("startTs");
Optional<Long> endTs = request.getLongParamValue("endTs"); Optional<Long> endTs = request.getLongParamValue("endTs");
Optional<Long> interval = request.getLongParamValue("interval");
Optional<Integer> limit = request.getIntParamValue("limit"); Optional<Integer> limit = request.getIntParamValue("limit");
Map<String, List<TsData>> data = new LinkedHashMap<>(); Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
for (String key : keys.split(",")) {
List<TsKvEntry> entries = ctx.loadTimeseries(deviceId, new BaseTsKvQuery(key, startTs, endTs, limit)); List<String> keys = Arrays.asList(keysStr.split(","));
data.put(key, entries.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList())); List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)).collect(Collectors.toList());
} ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
msg.getResponseHolder().setResult(new ResponseEntity<>(data, HttpStatus.OK)); @Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
Map<String, List<TsData>> result = new LinkedHashMap<>();
for (TsKvEntry entry : data) {
result.put(entry.getKey(), data.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList()));
}
msg.getResponseHolder().setResult(new ResponseEntity<>(data, HttpStatus.OK));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch historical data", e);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
});
} else if ("attributes".equals(entity)) { } else if ("attributes".equals(entity)) {
String keys = request.getParameter("keys", ""); String keys = request.getParameter("keys", "");
List<AttributeKvEntry> attributes;
PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
if (!StringUtils.isEmpty(scope)) { if (!StringUtils.isEmpty(scope)) {
attributes = getAttributeKvEntries(ctx, scope, deviceId, keys); if (!StringUtils.isEmpty(keys)) {
List<String> keyList = Arrays.asList(keys.split(","));
ctx.loadAttributes(deviceId, scope, keyList, callback);
} else {
ctx.loadAttributes(deviceId, scope, callback);
}
} else { } else {
attributes = new ArrayList<>(); if (!StringUtils.isEmpty(keys)) {
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(getAttributeKvEntries(ctx, s, deviceId, keys))); List<String> keyList = Arrays.asList(keys.split(","));
ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), keyList, callback);
} else {
ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
}
} }
List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
} }
} }
} else { } else {
@ -146,7 +165,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
} }
}); });
if (attributes.size() > 0) { if (attributes.size() > 0) {
ctx.saveAttributes(deviceId, scope, attributes, new PluginCallback<Void>() { ctx.saveAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, attributes, new PluginCallback<Void>() {
@Override @Override
public void onSuccess(PluginContext ctx, Void value) { public void onSuccess(PluginContext ctx, Void value) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
@ -155,6 +174,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
@Override @Override
public void onFailure(PluginContext ctx, Exception e) { public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to save attributes", e);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
} }
}); });
@ -163,8 +183,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
} }
} }
} }
} catch (IOException e) { } catch (IOException | RuntimeException e) {
log.debug("Failed to process POST request due to IO exception", e); log.debug("Failed to process POST request due to exception", e);
} }
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} }
@ -183,8 +203,18 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
String keysParam = request.getParameter("keys"); String keysParam = request.getParameter("keys");
if (!StringUtils.isEmpty(keysParam)) { if (!StringUtils.isEmpty(keysParam)) {
String[] keys = keysParam.split(","); String[] keys = keysParam.split(",");
ctx.removeAttributes(deviceId, scope, Arrays.asList(keys)); ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK)); @Override
public void onSuccess(PluginContext ctx, Void value) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to remove attributes", e);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
});
return; return;
} }
} }
@ -195,14 +225,37 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
} }
private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, String scope, DeviceId deviceId, String keysParam) {
List<AttributeKvEntry> attributes; private PluginCallback<List<AttributeKvEntry>> getAttributeKeysPluginCallback(final PluginRestMsg msg) {
if (!StringUtils.isEmpty(keysParam)) { return new PluginCallback<List<AttributeKvEntry>>() {
String[] keys = keysParam.split(","); @Override
attributes = ctx.loadAttributes(deviceId, scope, Arrays.asList(keys)); public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) {
} else { List<String> keys = attributes.stream().map(attrKv -> attrKv.getKey()).collect(Collectors.toList());
attributes = ctx.loadAttributes(deviceId, scope); msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
} }
return attributes;
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes", e);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
};
}
private PluginCallback<List<AttributeKvEntry>> getAttributeValuesPluginCallback(final PluginRestMsg msg) {
return new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) {
List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes", e);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
};
} }
} }

37
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.extensions.core.plugin.telemetry.handlers; package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.RuleId; import org.thingsboard.server.common.data.id.RuleId;
@ -38,6 +39,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler { public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
private final SubscriptionManager subscriptionManager; private final SubscriptionManager subscriptionManager;
@ -49,27 +51,36 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
public void handleGetAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, GetAttributesRequestRuleToPluginMsg msg) { public void handleGetAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, GetAttributesRequestRuleToPluginMsg msg) {
GetAttributesRequest request = msg.getPayload(); GetAttributesRequest request = msg.getPayload();
List<AttributeKvEntry> clientAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getClientAttributeNames()); BiPluginCallBack<List<AttributeKvEntry>, List<AttributeKvEntry>> callback = new BiPluginCallBack<List<AttributeKvEntry>, List<AttributeKvEntry>>() {
List<AttributeKvEntry> sharedAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.SHARED_SCOPE, request.getSharedAttributeNames());
BasicGetAttributesResponse response = BasicGetAttributesResponse.onSuccess(request.getMsgType(), @Override
request.getRequestId(), BasicAttributeKVMsg.from(clientAttributes, sharedAttributes)); public void onSuccess(PluginContext ctx, List<AttributeKvEntry> clientAttributes, List<AttributeKvEntry> sharedAttributes) {
BasicGetAttributesResponse response = BasicGetAttributesResponse.onSuccess(request.getMsgType(),
request.getRequestId(), BasicAttributeKVMsg.from(clientAttributes, sharedAttributes));
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response));
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to process get attributes request", e);
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e)));
}
};
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response)); getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getClientAttributeNames(), callback.getV1Callback());
getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.SHARED_SCOPE, request.getSharedAttributeNames(), callback.getV2Callback());
} }
private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Optional<Set<String>> names) { private void getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Optional<Set<String>> names, PluginCallback<List<AttributeKvEntry>> callback) {
List<AttributeKvEntry> attributes;
if (names.isPresent()) { if (names.isPresent()) {
if (!names.get().isEmpty()) { if (!names.get().isEmpty()) {
attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names.get())); ctx.loadAttributes(deviceId, scope, new ArrayList<>(names.get()), callback);
} else { } else {
attributes = ctx.loadAttributes(deviceId, scope); ctx.loadAttributes(deviceId, scope, callback);
} }
} else { } else {
attributes = Collections.emptyList(); callback.onSuccess(ctx, Collections.emptyList());
} }
return attributes;
} }
@Override @Override
@ -100,6 +111,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
@Override @Override
public void onFailure(PluginContext ctx, Exception e) { public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to process telemetry upload request", e);
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e))); ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e)));
} }
}); });
@ -108,7 +120,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
@Override @Override
public void handleUpdateAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, UpdateAttributesRequestRuleToPluginMsg msg) { public void handleUpdateAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, UpdateAttributesRequestRuleToPluginMsg msg) {
UpdateAttributesRequest request = msg.getPayload(); UpdateAttributesRequest request = msg.getPayload();
ctx.saveAttributes(msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getAttributes().stream().collect(Collectors.toList()), ctx.saveAttributes(msg.getTenantId(), msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getAttributes().stream().collect(Collectors.toList()),
new PluginCallback<Void>() { new PluginCallback<Void>() {
@Override @Override
public void onSuccess(PluginContext ctx, Void value) { public void onSuccess(PluginContext ctx, Void value) {
@ -127,6 +139,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
@Override @Override
public void onFailure(PluginContext ctx, Exception e) { public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to process attributes update request", e);
ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e))); ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e)));
} }
}); });

196
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java

@ -21,6 +21,7 @@ import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.*; import org.thingsboard.server.common.data.kv.*;
import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
import org.thingsboard.server.extensions.api.plugins.PluginCallback; import org.thingsboard.server.extensions.api.plugins.PluginCallback;
import org.thingsboard.server.extensions.api.plugins.PluginContext; import org.thingsboard.server.extensions.api.plugins.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.DefaultWebsocketMsgHandler; import org.thingsboard.server.extensions.api.plugins.handlers.DefaultWebsocketMsgHandler;
@ -47,6 +48,8 @@ import java.util.stream.Collectors;
public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
private static final int UNKNOWN_SUBSCRIPTION_ID = 0; private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
public static final int DEFAULT_LIMIT = 100;
public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
private final SubscriptionManager subscriptionManager; private final SubscriptionManager subscriptionManager;
@ -104,37 +107,70 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
SubscriptionState sub; SubscriptionState sub;
if (keysOptional.isPresent()) { if (keysOptional.isPresent()) {
List<String> keys = new ArrayList<>(keysOptional.get()); List<String> keys = new ArrayList<>(keysOptional.get());
List<AttributeKvEntry> data = new ArrayList<>();
PluginCallback<List<AttributeKvEntry>> callback = new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, 0L));
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes!", e);
SubscriptionUpdate update;
if (UnauthorizedException.class.isInstance(e)) {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
} else {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch attributes!");
}
sendWsMsg(ctx, sessionRef, update);
}
};
if (StringUtils.isEmpty(cmd.getScope())) { if (StringUtils.isEmpty(cmd.getScope())) {
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> data.addAll(ctx.loadAttributes(deviceId, s, keys))); ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), keys, callback);
} else { } else {
data.addAll(ctx.loadAttributes(deviceId, cmd.getScope(), keys)); ctx.loadAttributes(deviceId, cmd.getScope(), keys, callback);
} }
} else {
PluginCallback<List<AttributeKvEntry>> callback = new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> data) {
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); Map<String, Long> subState = new HashMap<>(attributesData.size());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
Map<String, Long> subState = new HashMap<>(keys.size()); SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
keys.forEach(key -> subState.put(key, 0L)); subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); }
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch attributes!", e);
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch attributes!");
sendWsMsg(ctx, sessionRef, update);
}
};
sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
} else {
List<AttributeKvEntry> data = new ArrayList<>();
if (StringUtils.isEmpty(cmd.getScope())) { if (StringUtils.isEmpty(cmd.getScope())) {
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> data.addAll(ctx.loadAttributes(deviceId, s))); ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
} else { } else {
data.addAll(ctx.loadAttributes(deviceId, cmd.getScope())); ctx.loadAttributes(deviceId, cmd.getScope(), callback);
} }
List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
Map<String, Long> subState = new HashMap<>(attributesData.size());
attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
} }
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
} }
} }
} }
@ -153,45 +189,17 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
if (keysOptional.isPresent()) { if (keysOptional.isPresent()) {
long startTs; long startTs;
if (cmd.getTimeWindow() > 0) { if (cmd.getTimeWindow() > 0) {
List<TsKvEntry> data = new ArrayList<>();
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId()); log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
long endTs = System.currentTimeMillis(); startTs = cmd.getStartTs();
startTs = endTs - cmd.getTimeWindow(); long endTs = cmd.getStartTs() + cmd.getTimeWindow();
for (String key : keys) { List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
TsKvQuery query = new BaseTsKvQuery(key, startTs, endTs); ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
data.addAll(ctx.loadTimeseries(deviceId, query));
}
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
} else { } else {
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
startTs = System.currentTimeMillis(); startTs = System.currentTimeMillis();
log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), cmd.getDeviceId()); log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
ctx.loadLatestTimeseries(deviceId, keys, new PluginCallback<List<TsKvEntry>>() { ctx.loadLatestTimeseries(deviceId, keys, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
sendWsMsg(ctx, sessionRef, update);
}
});
} }
} else { } else {
ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() { ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
@ -206,8 +214,14 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
@Override @Override
public void onFailure(PluginContext ctx, Exception e) { public void onFailure(PluginContext ctx, Exception e) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, SubscriptionUpdate update;
"Failed to fetch data!"); if (UnauthorizedException.class.isInstance(e)) {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
} else {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
}
sendWsMsg(ctx, sessionRef, update); sendWsMsg(ctx, sessionRef, update);
} }
}); });
@ -216,6 +230,29 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
} }
} }
private PluginCallback<List<TsKvEntry>> getSubscriptionCallback(final PluginWebsocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final DeviceId deviceId, final long startTs, final List<String> keys) {
return new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
Map<String, Long> subState = new HashMap<>(keys.size());
keys.forEach(key -> subState.put(key, startTs));
data.forEach(v -> subState.put(v.getKey(), v.getTs()));
SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to fetch data!", e);
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
sendWsMsg(ctx, sessionRef, update);
}
};
}
private void handleWsHistoryCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, GetHistoryCmd cmd) { private void handleWsHistoryCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, GetHistoryCmd cmd) {
String sessionId = sessionRef.getSessionId(); String sessionId = sessionRef.getSessionId();
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
@ -239,19 +276,35 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
return; return;
} }
DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId()); DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
if (!ctx.checkAccess(deviceId)) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
sendWsMsg(ctx, sessionRef, update);
return;
}
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet())); List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
List<TsKvEntry> data = new ArrayList<>(); List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
for (String key : keys) { ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
TsKvQuery query = new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs()); @Override
data.addAll(ctx.loadTimeseries(deviceId, query)); public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
} sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); }
@Override
public void onFailure(PluginContext ctx, Exception e) {
SubscriptionUpdate update;
if (UnauthorizedException.class.isInstance(e)) {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
} else {
update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
}
sendWsMsg(ctx, sessionRef, update);
}
});
}
private static Aggregation getAggregation(String agg) {
return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg);
}
private int getLimit(int limit) {
return limit == 0 ? DEFAULT_LIMIT : limit;
} }
private boolean validateSessionMetadata(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { private boolean validateSessionMetadata(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
@ -282,13 +335,6 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, update); sendWsMsg(ctx, sessionRef, update);
return false; return false;
} }
DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
if (!ctx.checkAccess(deviceId)) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
sendWsMsg(ctx, sessionRef, update);
return false;
}
return true; return true;
} }

36
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java

@ -17,6 +17,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Getter;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import java.util.Map; import java.util.Map;
@ -24,16 +25,37 @@ import java.util.Map;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@Data
@AllArgsConstructor @AllArgsConstructor
public class SubscriptionState { public class SubscriptionState {
private final String wsSessionId; @Getter private final String wsSessionId;
private final int subscriptionId; @Getter private final int subscriptionId;
private final DeviceId deviceId; @Getter private final DeviceId deviceId;
private final SubscriptionType type; @Getter private final SubscriptionType type;
private final boolean allKeys; @Getter private final boolean allKeys;
private final Map<String, Long> keyStates; @Getter private final Map<String, Long> keyStates;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SubscriptionState that = (SubscriptionState) o;
if (subscriptionId != that.subscriptionId) return false;
if (wsSessionId != null ? !wsSessionId.equals(that.wsSessionId) : that.wsSessionId != null) return false;
if (deviceId != null ? !deviceId.equals(that.deviceId) : that.deviceId != null) return false;
return type == that.type;
}
@Override
public int hashCode() {
int result = wsSessionId != null ? wsSessionId.hashCode() : 0;
result = 31 * result + subscriptionId;
result = 31 * result + (deviceId != null ? deviceId.hashCode() : 0);
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
@Override @Override
public String toString() { public String toString() {

2
extensions/extension-kafka/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>extensions</artifactId> <artifactId>extensions</artifactId>
</parent> </parent>
<groupId>org.thingsboard.extensions</groupId> <groupId>org.thingsboard.extensions</groupId>

2
extensions/extension-rabbitmq/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>extensions</artifactId> <artifactId>extensions</artifactId>
</parent> </parent>
<groupId>org.thingsboard.extensions</groupId> <groupId>org.thingsboard.extensions</groupId>

2
extensions/extension-rest-api-call/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>extensions</artifactId> <artifactId>extensions</artifactId>
</parent> </parent>
<groupId>org.thingsboard.extensions</groupId> <groupId>org.thingsboard.extensions</groupId>

2
extensions/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Thingsboard</name> <name>Thingsboard</name>

2
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

4
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java

@ -89,7 +89,9 @@ public class GatewaySessionCtx {
public void onDeviceDisconnect(MqttPublishMessage msg) throws AdaptorException { public void onDeviceDisconnect(MqttPublishMessage msg) throws AdaptorException {
String deviceName = checkDeviceName(getDeviceName(msg)); String deviceName = checkDeviceName(getDeviceName(msg));
GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName);
deviceSessionCtx.setClosed(true); if (deviceSessionCtx != null) {
deviceSessionCtx.setClosed(true);
}
ack(msg); ack(msg);
} }

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

10
ui/package.json

@ -1,7 +1,7 @@
{ {
"name": "thingsboard", "name": "thingsboard",
"private": true, "private": true,
"version": "1.1.0", "version": "1.2.0",
"description": "Thingsboard UI", "description": "Thingsboard UI",
"licenses": [ "licenses": [
{ {
@ -48,11 +48,12 @@
"canvas-gauges": "^2.0.9", "canvas-gauges": "^2.0.9",
"clipboard": "^1.5.15", "clipboard": "^1.5.15",
"compass-sass-mixins": "^0.12.7", "compass-sass-mixins": "^0.12.7",
"flot": "git://github.com/flot/flot.git#0.9-work",
"font-awesome": "^4.6.3", "font-awesome": "^4.6.3",
"javascript-detect-element-resize": "^0.5.3",
"jquery": "^3.1.0", "jquery": "^3.1.0",
"js-beautify": "^1.6.4", "js-beautify": "^1.6.4",
"json-schema-defaults": "^0.2.0", "json-schema-defaults": "^0.2.0",
"justgage": "^1.2.2",
"leaflet": "^1.0.3", "leaflet": "^1.0.3",
"material-ui": "^0.16.1", "material-ui": "^0.16.1",
"material-ui-number-input": "^5.0.16", "material-ui-number-input": "^5.0.16",
@ -63,7 +64,6 @@
"ngreact": "^0.3.0", "ngreact": "^0.3.0",
"objectpath": "^1.2.1", "objectpath": "^1.2.1",
"oclazyload": "^1.0.9", "oclazyload": "^1.0.9",
"raphael": "^2.2.7",
"rc-select": "^6.6.1", "rc-select": "^6.6.1",
"react": "^15.4.1", "react": "^15.4.1",
"react-ace": "^4.1.0", "react-ace": "^4.1.0",
@ -76,6 +76,7 @@
"schema-inspector": "^1.6.6", "schema-inspector": "^1.6.6",
"split.js": "^1.0.7", "split.js": "^1.0.7",
"tinycolor2": "^1.4.1", "tinycolor2": "^1.4.1",
"typeface-roboto": "0.0.22",
"v-accordion": "^1.6.0" "v-accordion": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
@ -113,7 +114,8 @@
"webpack": "^1.13.2", "webpack": "^1.13.2",
"webpack-dev-middleware": "^1.6.1", "webpack-dev-middleware": "^1.6.1",
"webpack-dev-server": "^1.15.1", "webpack-dev-server": "^1.15.1",
"webpack-hot-middleware": "^2.12.2" "webpack-hot-middleware": "^2.12.2",
"webpack-material-design-icons": "^0.1.0"
}, },
"engine": "node >= 5.9.0", "engine": "node >= 5.9.0",
"nyc": { "nyc": {

2
ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version> <version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
ui/src/app/admin/general-settings.tpl.html

@ -22,7 +22,7 @@
<span translate class="md-headline">admin.general-settings</span> <span translate class="md-headline">admin.general-settings</span>
</md-card-title-text> </md-card-title-text>
</md-card-title> </md-card-title>
<md-progress-linear md-mode="indeterminate" ng-show="loading"></md-progress-linear> <md-progress-linear md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span> <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-card-content> <md-card-content>
<form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm"> <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">

2
ui/src/app/admin/outgoing-mail-settings.tpl.html

@ -24,7 +24,7 @@
<div id="help-container"></div> <div id="help-container"></div>
</md-card-title-text> </md-card-title-text>
</md-card-title> </md-card-title>
<md-progress-linear md-mode="indeterminate" ng-show="loading"></md-progress-linear> <md-progress-linear md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span> <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-card-content> <md-card-content>
<form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm"> <form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm">

16
ui/src/app/api/dashboard.service.js

@ -22,6 +22,7 @@ function DashboardService($http, $q) {
var service = { var service = {
assignDashboardToCustomer: assignDashboardToCustomer, assignDashboardToCustomer: assignDashboardToCustomer,
getCustomerDashboards: getCustomerDashboards, getCustomerDashboards: getCustomerDashboards,
getServerTimeDiff: getServerTimeDiff,
getDashboard: getDashboard, getDashboard: getDashboard,
getTenantDashboards: getTenantDashboards, getTenantDashboards: getTenantDashboards,
deleteDashboard: deleteDashboard, deleteDashboard: deleteDashboard,
@ -71,6 +72,21 @@ function DashboardService($http, $q) {
return deferred.promise; return deferred.promise;
} }
function getServerTimeDiff() {
var deferred = $q.defer();
var url = '/api/dashboard/serverTime';
var ct1 = Date.now();
$http.get(url, null).then(function success(response) {
var ct2 = Date.now();
var st = response.data;
var stDiff = Math.ceil(st - (ct1+ct2)/2);
deferred.resolve(stDiff);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function getDashboard(dashboardId) { function getDashboard(dashboardId) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/dashboard/' + dashboardId; var url = '/api/dashboard/' + dashboardId;

274
ui/src/app/api/data-aggregator.js

@ -0,0 +1,274 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default class DataAggregator {
constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval, types, $timeout, $filter) {
this.onDataCb = onDataCb;
this.tsKeyNames = tsKeyNames;
this.startTs = startTs;
this.aggregationType = aggregationType;
this.types = types;
this.$timeout = $timeout;
this.$filter = $filter;
this.dataReceived = false;
this.resetPending = false;
this.noAggregation = aggregationType === types.aggregation.none.value;
this.limit = limit;
this.timeWindow = timeWindow;
this.interval = interval;
this.aggregationTimeout = Math.max(this.interval, 1000);
switch (aggregationType) {
case types.aggregation.min.value:
this.aggFunction = min;
break;
case types.aggregation.max.value:
this.aggFunction = max;
break;
case types.aggregation.avg.value:
this.aggFunction = avg;
break;
case types.aggregation.sum.value:
this.aggFunction = sum;
break;
case types.aggregation.count.value:
this.aggFunction = count;
break;
case types.aggregation.none.value:
this.aggFunction = none;
break;
default:
this.aggFunction = avg;
}
}
reset(startTs, timeWindow, interval) {
if (this.intervalTimeoutHandle) {
this.$timeout.cancel(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
this.intervalScheduledTime = currentTime();
this.startTs = startTs;
this.timeWindow = timeWindow;
this.interval = interval;
this.endTs = this.startTs + this.timeWindow;
this.elapsed = 0;
this.aggregationTimeout = Math.max(this.interval, 1000);
this.resetPending = true;
var self = this;
this.intervalTimeoutHandle = this.$timeout(function() {
self.onInterval();
}, this.aggregationTimeout, false);
}
onData(data, update, history, apply) {
if (!this.dataReceived || this.resetPending) {
var updateIntervalScheduledTime = true;
if (!this.dataReceived) {
this.elapsed = 0;
this.dataReceived = true;
this.endTs = this.startTs + this.timeWindow;
}
if (this.resetPending) {
this.resetPending = false;
updateIntervalScheduledTime = false;
}
if (update) {
this.aggregationMap = {};
updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
} else {
this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation);
}
if (updateIntervalScheduledTime) {
this.intervalScheduledTime = currentTime();
}
this.onInterval(history, apply);
} else {
updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value,
this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs);
if (history) {
this.intervalScheduledTime = currentTime();
this.onInterval(history, apply);
}
}
}
onInterval(history, apply) {
var now = currentTime();
this.elapsed += now - this.intervalScheduledTime;
this.intervalScheduledTime = now;
if (this.intervalTimeoutHandle) {
this.$timeout.cancel(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
if (!history) {
var delta = Math.floor(this.elapsed / this.interval);
if (delta || !this.data) {
this.startTs += delta * this.interval;
this.endTs += delta * this.interval;
this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
this.elapsed = this.elapsed - delta * this.interval;
}
} else {
this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit);
}
if (this.onDataCb) {
this.onDataCb(this.data, this.startTs, this.endTs, apply);
}
var self = this;
if (!history) {
this.intervalTimeoutHandle = this.$timeout(function() {
self.onInterval();
}, this.aggregationTimeout, false);
}
}
destroy() {
if (this.intervalTimeoutHandle) {
this.$timeout.cancel(this.intervalTimeoutHandle);
this.intervalTimeoutHandle = null;
}
this.aggregationMap = null;
}
}
/* eslint-disable */
function currentTime() {
return window.performance && window.performance.now ?
window.performance.now() : Date.now();
}
/* eslint-enable */
function processAggregatedData(data, isCount, noAggregation) {
var aggregationMap = {};
for (var key in data) {
var aggKeyData = aggregationMap[key];
if (!aggKeyData) {
aggKeyData = {};
aggregationMap[key] = aggKeyData;
}
var keyData = data[key];
for (var i in keyData) {
var kvPair = keyData[i];
var timestamp = kvPair[0];
var value = convertValue(kvPair[1], noAggregation);
var aggKey = timestamp;
var aggData = {
count: isCount ? value : 1,
sum: value,
aggValue: value
}
aggKeyData[aggKey] = aggData;
}
}
return aggregationMap;
}
function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunction, data, interval, startTs) {
for (var key in data) {
var aggKeyData = aggregationMap[key];
if (!aggKeyData) {
aggKeyData = {};
aggregationMap[key] = aggKeyData;
}
var keyData = data[key];
for (var i in keyData) {
var kvPair = keyData[i];
var timestamp = kvPair[0];
var value = convertValue(kvPair[1], noAggregation);
var aggTimestamp = noAggregation ? timestamp : (startTs + Math.floor((timestamp - startTs) / interval) * interval + interval/2);
var aggData = aggKeyData[aggTimestamp];
if (!aggData) {
aggData = {
count: 1,
sum: value,
aggValue: isCount ? 1 : value
}
aggKeyData[aggTimestamp] = aggData;
} else {
aggFunction(aggData, value);
}
}
}
}
function toData(tsKeyNames, aggregationMap, startTs, endTs, $filter, limit) {
var data = {};
for (var k in tsKeyNames) {
data[tsKeyNames[k]] = [];
}
for (var key in aggregationMap) {
var aggKeyData = aggregationMap[key];
var keyData = data[key];
for (var aggTimestamp in aggKeyData) {
if (aggTimestamp <= startTs) {
delete aggKeyData[aggTimestamp];
} else if (aggTimestamp <= endTs) {
var aggData = aggKeyData[aggTimestamp];
var kvPair = [Number(aggTimestamp), aggData.aggValue];
keyData.push(kvPair);
}
}
keyData = $filter('orderBy')(keyData, '+this[0]');
if (keyData.length > limit) {
keyData = keyData.slice(keyData.length - limit);
}
data[key] = keyData;
}
return data;
}
function convertValue(value, noAggregation) {
if (!noAggregation || value && isNumeric(value)) {
return Number(value);
} else {
return value;
}
}
function isNumeric(value) {
return (value - parseFloat( value ) + 1) >= 0;
}
function avg(aggData, value) {
aggData.count++;
aggData.sum += value;
aggData.aggValue = aggData.sum / aggData.count;
}
function min(aggData, value) {
aggData.aggValue = Math.min(aggData.aggValue, value);
}
function max(aggData, value) {
aggData.aggValue = Math.max(aggData.aggValue, value);
}
function sum(aggData, value) {
aggData.aggValue = aggData.aggValue + value;
}
function count(aggData) {
aggData.count++;
aggData.aggValue = aggData.count;
}
function none(aggData, value) {
aggData.aggValue = value;
}

309
ui/src/app/api/datasource.service.js

@ -17,13 +17,14 @@ import thingsboardApiDevice from './device.service';
import thingsboardApiTelemetryWebsocket from './telemetry-websocket.service'; import thingsboardApiTelemetryWebsocket from './telemetry-websocket.service';
import thingsboardTypes from '../common/types.constant'; import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service'; import thingsboardUtils from '../common/utils.service';
import DataAggregator from './data-aggregator';
export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils]) export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils])
.factory('datasourceService', DatasourceService) .factory('datasourceService', DatasourceService)
.name; .name;
/*@ngInject*/ /*@ngInject*/
function DatasourceService($timeout, $log, telemetryWebsocketService, types, utils) { function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, types, utils) {
var subscriptions = {}; var subscriptions = {};
@ -73,7 +74,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti
subscription = subscriptions[listener.datasourceSubscriptionKey]; subscription = subscriptions[listener.datasourceSubscriptionKey];
subscription.syncListener(listener); subscription.syncListener(listener);
} else { } else {
subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils); subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils);
subscriptions[listener.datasourceSubscriptionKey] = subscription; subscriptions[listener.datasourceSubscriptionKey] = subscription;
subscription.start(); subscription.start();
} }
@ -96,7 +97,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti
} }
function DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils) { function DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils) {
var listeners = []; var listeners = [];
var datasourceType = datasourceSubscription.datasourceType; var datasourceType = datasourceSubscription.datasourceType;
@ -107,9 +108,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
datasourceSubscription.subscriptionTimewindow.fixedWindow; datasourceSubscription.subscriptionTimewindow.fixedWindow;
var realtime = datasourceSubscription.subscriptionTimewindow && var realtime = datasourceSubscription.subscriptionTimewindow &&
datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
var dataGenFunction = null;
var timer; var timer;
var frequency; var frequency;
var dataAggregator;
var subscription = { var subscription = {
addListener: addListener, addListener: addListener,
@ -130,42 +131,42 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
dataKey.index = i; dataKey.index = i;
var key; var key;
if (datasourceType === types.datasourceType.function) { if (datasourceType === types.datasourceType.function) {
key = utils.objectHashCode(dataKey);
if (!dataKey.func) { if (!dataKey.func) {
dataKey.func = new Function("time", "prevValue", dataKey.funcBody); dataKey.func = new Function("time", "prevValue", dataKey.funcBody);
} }
datasourceData[key] = []; } else {
dataKeys[key] = dataKey;
} else if (datasourceType === types.datasourceType.device) {
key = dataKey.name + '_' + dataKey.type;
if (dataKey.postFuncBody && !dataKey.postFunc) { if (dataKey.postFuncBody && !dataKey.postFunc) {
dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody); dataKey.postFunc = new Function("time", "value", "prevValue", dataKey.postFuncBody);
} }
}
if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
if (datasourceType === types.datasourceType.function) {
key = dataKey.name + '_' + dataKey.index + '_' + dataKey.type;
} else {
key = dataKey.name + '_' + dataKey.type;
}
var dataKeysList = dataKeys[key]; var dataKeysList = dataKeys[key];
if (!dataKeysList) { if (!dataKeysList) {
dataKeysList = []; dataKeysList = [];
dataKeys[key] = dataKeysList; dataKeys[key] = dataKeysList;
} }
var index = dataKeysList.push(dataKey) - 1; var index = dataKeysList.push(dataKey) - 1;
datasourceData[key + '_' + index] = []; datasourceData[key + '_' + index] = {
data: []
};
} else {
key = utils.objectHashCode(dataKey);
datasourceData[key] = {
data: []
};
dataKeys[key] = dataKey;
} }
dataKey.key = key; dataKey.key = key;
} }
if (datasourceType === types.datasourceType.function) { if (datasourceType === types.datasourceType.function) {
frequency = 1000; frequency = 1000;
if (datasourceSubscription.type === types.widgetType.timeseries.value) { if (datasourceSubscription.type === types.widgetType.timeseries.value) {
dataGenFunction = generateSeries; frequency = Math.min(datasourceSubscription.subscriptionTimewindow.aggregation.interval, 5000);
var window;
if (realtime) {
window = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
} else {
window = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs -
datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
}
frequency = window / 1000 * 20;
} else if (datasourceSubscription.type === types.widgetType.latest.value) {
dataGenFunction = generateLatest;
frequency = 1000;
} }
} }
} }
@ -188,14 +189,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
function syncListener(listener) { function syncListener(listener) {
var key; var key;
var dataKey; var dataKey;
if (datasourceType === types.datasourceType.function) { if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
for (key in dataKeys) {
dataKey = dataKeys[key];
listener.dataUpdated(datasourceData[key],
listener.datasourceIndex,
dataKey.index);
}
} else if (datasourceType === types.datasourceType.device) {
for (key in dataKeys) { for (key in dataKeys) {
var dataKeysList = dataKeys[key]; var dataKeysList = dataKeys[key];
for (var i = 0; i < dataKeysList.length; i++) { for (var i = 0; i < dataKeysList.length; i++) {
@ -203,9 +197,16 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
var datasourceKey = key + '_' + i; var datasourceKey = key + '_' + i;
listener.dataUpdated(datasourceData[datasourceKey], listener.dataUpdated(datasourceData[datasourceKey],
listener.datasourceIndex, listener.datasourceIndex,
dataKey.index); dataKey.index, false);
} }
} }
} else {
for (key in dataKeys) {
dataKey = dataKeys[key];
listener.dataUpdated(datasourceData[key],
listener.datasourceIndex,
dataKey.index, false);
}
} }
} }
@ -213,7 +214,10 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
if (history && !hasListeners()) { if (history && !hasListeners()) {
return; return;
} }
//$log.debug("started!"); var subsTw = datasourceSubscription.subscriptionTimewindow;
var tsKeyNames = [];
var dataKey;
if (datasourceType === types.datasourceType.device) { if (datasourceType === types.datasourceType.device) {
//send subscribe command //send subscribe command
@ -223,12 +227,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
for (var key in dataKeys) { for (var key in dataKeys) {
var dataKeysList = dataKeys[key]; var dataKeysList = dataKeys[key];
var dataKey = dataKeysList[0]; dataKey = dataKeysList[0];
if (dataKey.type === types.dataKeyType.timeseries) { if (dataKey.type === types.dataKeyType.timeseries) {
if (tsKeys.length > 0) { if (tsKeys.length > 0) {
tsKeys += ','; tsKeys += ',';
} }
tsKeys += dataKey.name; tsKeys += dataKey.name;
tsKeyNames.push(dataKey.name);
} else if (dataKey.type === types.dataKeyType.attribute) { } else if (dataKey.type === types.dataKeyType.attribute) {
if (attrKeys.length > 0) { if (attrKeys.length > 0) {
attrKeys += ','; attrKeys += ',';
@ -247,19 +252,22 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
var historyCommand = { var historyCommand = {
deviceId: datasourceSubscription.deviceId, deviceId: datasourceSubscription.deviceId,
keys: tsKeys, keys: tsKeys,
startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs, startTs: subsTw.fixedWindow.startTimeMs,
endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs endTs: subsTw.fixedWindow.endTimeMs,
interval: subsTw.aggregation.interval,
limit: subsTw.aggregation.limit,
agg: subsTw.aggregation.type
}; };
subscriber = { subscriber = {
historyCommand: historyCommand, historyCommand: historyCommand,
type: types.dataKeyType.timeseries, type: types.dataKeyType.timeseries,
onData: function (data) { onData: function (data) {
onData(data, types.dataKeyType.timeseries); if (data.data) {
onData(data.data, types.dataKeyType.timeseries, null, null, true);
}
}, },
onReconnected: function() { onReconnected: function() {}
onReconnected();
}
}; };
telemetryWebsocketService.subscribe(subscriber); telemetryWebsocketService.subscribe(subscriber);
@ -272,21 +280,39 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
keys: tsKeys keys: tsKeys
}; };
if (datasourceSubscription.type === types.widgetType.timeseries.value) {
subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
}
subscriber = { subscriber = {
subscriptionCommand: subscriptionCommand, subscriptionCommand: subscriptionCommand,
type: types.dataKeyType.timeseries, type: types.dataKeyType.timeseries
onData: function (data) {
onData(data, types.dataKeyType.timeseries);
},
onReconnected: function() {
onReconnected();
}
}; };
if (datasourceSubscription.type === types.widgetType.timeseries.value) {
updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw);
dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.timeseries);
subscriber.onData = function(data) {
dataAggregator.onData(data, false, false, true);
}
subscriber.onReconnected = function() {
var newSubsTw = null;
for (var i2 in listeners) {
var listener = listeners[i2];
if (!newSubsTw) {
newSubsTw = listener.updateRealtimeSubscription();
} else {
listener.setRealtimeSubscription(newSubsTw);
}
}
updateRealtimeSubscriptionCommand(this.subscriptionCommand, newSubsTw);
dataAggregator.reset(newSubsTw.startTs, newSubsTw.aggregation.timeWindow, newSubsTw.aggregation.interval);
}
} else {
subscriber.onReconnected = function() {}
subscriber.onData = function(data) {
if (data.data) {
onData(data.data, types.dataKeyType.timeseries, null, null, true);
}
}
}
telemetryWebsocketService.subscribe(subscriber); telemetryWebsocketService.subscribe(subscriber);
subscribers[subscriber.subscriptionCommand.cmdId] = subscriber; subscribers[subscriber.subscriptionCommand.cmdId] = subscriber;
@ -304,11 +330,11 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
subscriptionCommand: subscriptionCommand, subscriptionCommand: subscriptionCommand,
type: types.dataKeyType.attribute, type: types.dataKeyType.attribute,
onData: function (data) { onData: function (data) {
onData(data, types.dataKeyType.attribute); if (data.data) {
onData(data.data, types.dataKeyType.attribute, null, null, true);
}
}, },
onReconnected: function() { onReconnected: function() {}
onReconnected();
}
}; };
telemetryWebsocketService.subscribe(subscriber); telemetryWebsocketService.subscribe(subscriber);
@ -316,14 +342,52 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
} }
} else if (dataGenFunction) { } else if (datasourceType === types.datasourceType.function) {
if (datasourceSubscription.type === types.widgetType.timeseries.value) {
for (key in dataKeys) {
var dataKeyList = dataKeys[key];
for (var index = 0; index < dataKeyList.length; index++) {
dataKey = dataKeyList[index];
tsKeyNames.push(dataKey.name+'_'+dataKey.index);
}
}
dataAggregator = createRealtimeDataAggregator(subsTw, tsKeyNames, types.dataKeyType.function);
}
if (history) { if (history) {
onTick(); onTick(false);
} else { } else {
timer = $timeout(onTick, 0, false); timer = $timeout(
function() {onTick(true)},
0,
false
);
} }
} }
}
function createRealtimeDataAggregator(subsTw, tsKeyNames, dataKeyType) {
return new DataAggregator(
function(data, startTs, endTs, apply) {
onData(data, dataKeyType, startTs, endTs, apply);
},
tsKeyNames,
subsTw.startTs,
subsTw.aggregation.limit,
subsTw.aggregation.type,
subsTw.aggregation.timeWindow,
subsTw.aggregation.interval,
types,
$timeout,
$filter
);
}
function updateRealtimeSubscriptionCommand(subscriptionCommand, subsTw) {
subscriptionCommand.startTs = subsTw.startTs;
subscriptionCommand.timeWindow = subsTw.aggregation.timeWindow;
subscriptionCommand.interval = subsTw.aggregation.interval;
subscriptionCommand.limit = subsTw.aggregation.limit;
subscriptionCommand.agg = subsTw.aggregation.type;
} }
function unsubscribe() { function unsubscribe() {
@ -332,35 +396,25 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
} }
if (datasourceType === types.datasourceType.device) { if (datasourceType === types.datasourceType.device) {
for (var cmdId in subscribers) { for (var cmdId in subscribers) {
telemetryWebsocketService.unsubscribe(subscribers[cmdId]); var subscriber = subscribers[cmdId];
telemetryWebsocketService.unsubscribe(subscriber);
if (subscriber.onDestroy) {
subscriber.onDestroy();
}
} }
subscribers = {}; subscribers = {};
} }
//$log.debug("unsibscribed!"); if (dataAggregator) {
} dataAggregator.destroy();
dataAggregator = null;
function boundToInterval(data, timewindowMs) {
if (data.length > 1) {
var start = data[0][0];
var end = data[data.length - 1][0];
var i = 0;
var currentInterval = end - start;
while (currentInterval > timewindowMs && i < data.length - 2) {
i++;
start = data[i][0];
currentInterval = end - start;
}
if (i > 1) {
data.splice(0, i - 1);
}
} }
return data;
} }
function generateSeries(dataKey, startTime, endTime) { function generateSeries(dataKey, index, startTime, endTime) {
var data = []; var data = [];
var prevSeries; var prevSeries;
var datasourceKeyData = datasourceData[dataKey.key]; var datasourceDataKey = dataKey.key + '_' + index;
var datasourceKeyData = datasourceData[datasourceDataKey].data;
if (datasourceKeyData.length > 0) { if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else { } else {
@ -377,23 +431,12 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
if (data.length > 0) { if (data.length > 0) {
dataKey.lastUpdateTime = data[data.length - 1][0]; dataKey.lastUpdateTime = data[data.length - 1][0];
} }
if (realtime) { return data;
datasourceData[dataKey.key] = boundToInterval(datasourceKeyData.concat(data),
datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
} else {
datasourceData[dataKey.key] = data;
}
for (var i in listeners) {
var listener = listeners[i];
listener.dataUpdated(datasourceData[dataKey.key],
listener.datasourceIndex,
dataKey.index);
}
} }
function generateLatest(dataKey) { function generateLatest(dataKey, apply) {
var prevSeries; var prevSeries;
var datasourceKeyData = datasourceData[dataKey.key]; var datasourceKeyData = datasourceData[dataKey.key].data;
if (datasourceKeyData.length > 0) { if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else { } else {
@ -404,64 +447,54 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
series.push(time); series.push(time);
var value = dataKey.func(time, prevSeries[1]); var value = dataKey.func(time, prevSeries[1]);
series.push(value); series.push(value);
datasourceData[dataKey.key] = [series]; datasourceData[dataKey.key].data = [series];
for (var i in listeners) { for (var i in listeners) {
var listener = listeners[i]; var listener = listeners[i];
listener.dataUpdated(datasourceData[dataKey.key], listener.dataUpdated(datasourceData[dataKey.key],
listener.datasourceIndex, listener.datasourceIndex,
dataKey.index); dataKey.index, apply);
} }
} }
function onTick() { function onTick(apply) {
var key; var key;
if (datasourceSubscription.type === types.widgetType.timeseries.value) { if (datasourceSubscription.type === types.widgetType.timeseries.value) {
var startTime; var startTime;
var endTime; var endTime;
var generatedData = {
data: {
}
};
for (key in dataKeys) { for (key in dataKeys) {
var dataKey = dataKeys[key]; var dataKeyList = dataKeys[key];
if (!startTime) { for (var index = 0; index < dataKeyList.length; index ++) {
if (realtime) { var dataKey = dataKeyList[index];
endTime = (new Date).getTime(); if (!startTime) {
if (dataKey.lastUpdateTime) { if (realtime) {
startTime = dataKey.lastUpdateTime + frequency; if (dataKey.lastUpdateTime) {
startTime = dataKey.lastUpdateTime + frequency
} else {
startTime = datasourceSubscription.subscriptionTimewindow.startTs;
}
endTime = startTime + datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
} else { } else {
startTime = endTime - datasourceSubscription.subscriptionTimewindow.realtimeWindowMs; startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
} }
} else {
startTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs;
endTime = datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs;
} }
var data = generateSeries(dataKey, index, startTime, endTime);
generatedData.data[dataKey.name+'_'+dataKey.index] = data;
} }
generateSeries(dataKey, startTime, endTime);
} }
dataAggregator.onData(generatedData, true, history, apply);
} else if (datasourceSubscription.type === types.widgetType.latest.value) { } else if (datasourceSubscription.type === types.widgetType.latest.value) {
for (key in dataKeys) { for (key in dataKeys) {
generateLatest(dataKeys[key]); generateLatest(dataKeys[key], apply);
} }
} }
if (!history) { if (!history) {
timer = $timeout(onTick, frequency / 2, false); timer = $timeout(function() {onTick(true)}, frequency / 2, false);
}
}
function onReconnected() {
if (datasourceType === types.datasourceType.device) {
for (var key in dataKeys) {
var dataKeysList = dataKeys[key];
for (var i = 0; i < dataKeysList.length; i++) {
var dataKey = dataKeysList[i];
var datasourceKey = key + '_' + i;
datasourceData[datasourceKey] = [];
for (var l in listeners) {
var listener = listeners[l];
listener.dataUpdated(datasourceData[datasourceKey],
listener.datasourceIndex,
dataKey.index);
}
}
}
} }
} }
@ -477,18 +510,23 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
} }
} }
function onData(sourceData, type) { function onData(sourceData, type, startTs, endTs, apply) {
for (var keyName in sourceData) { for (var keyName in sourceData) {
var keyData = sourceData[keyName]; var keyData = sourceData[keyName];
var key = keyName + '_' + type; var key = keyName + '_' + type;
var dataKeyList = dataKeys[key]; var dataKeyList = dataKeys[key];
for (var keyIndex = 0; keyIndex < dataKeyList.length; keyIndex++) { for (var keyIndex = 0; keyIndex < dataKeyList.length; keyIndex++) {
var datasourceKey = key + "_" + keyIndex; var datasourceKey = key + "_" + keyIndex;
if (datasourceData[datasourceKey]) { if (datasourceData[datasourceKey].data) {
var dataKey = dataKeyList[keyIndex]; var dataKey = dataKeyList[keyIndex];
var data = []; var data = [];
var prevSeries; var prevSeries;
var datasourceKeyData = datasourceData[datasourceKey]; var datasourceKeyData;
if (realtime) {
datasourceKeyData = [];
} else {
datasourceKeyData = datasourceData[datasourceKey].data;
}
if (datasourceKeyData.length > 0) { if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else { } else {
@ -519,17 +557,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
data.push(series); data.push(series);
} }
} }
if (data.length > 0) { if (data || (startTs && endTs)) {
if (realtime) { datasourceData[datasourceKey].data = data;
datasourceData[datasourceKey] = boundToInterval(datasourceKeyData.concat(data), datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
} else {
datasourceData[datasourceKey] = data;
}
for (var i2 in listeners) { for (var i2 in listeners) {
var listener = listeners[i2]; var listener = listeners[i2];
listener.dataUpdated(datasourceData[datasourceKey], listener.dataUpdated(datasourceData[datasourceKey],
listener.datasourceIndex, listener.datasourceIndex,
dataKey.index); dataKey.index, apply);
} }
} }
} }
@ -537,3 +571,4 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
} }
} }
} }

213
ui/src/app/api/device.service.js

@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function DeviceService($http, $q, $filter, telemetryWebsocketService, types) { function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
var deviceAttributesSubscriptionMap = {}; var deviceAttributesSubscriptionMap = {};
@ -30,6 +30,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
deleteDevice: deleteDevice, deleteDevice: deleteDevice,
getCustomerDevices: getCustomerDevices, getCustomerDevices: getCustomerDevices,
getDevice: getDevice, getDevice: getDevice,
getDevices: getDevices,
processDeviceAliases: processDeviceAliases,
checkDeviceAlias: checkDeviceAlias,
getDeviceCredentials: getDeviceCredentials, getDeviceCredentials: getDeviceCredentials,
getDeviceKeys: getDeviceKeys, getDeviceKeys: getDeviceKeys,
getDeviceTimeseriesValues: getDeviceTimeseriesValues, getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@ -99,6 +102,200 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
return deferred.promise; return deferred.promise;
} }
function getDevices(deviceIds) {
var deferred = $q.defer();
var ids = '';
for (var i=0;i<deviceIds.length;i++) {
if (i>0) {
ids += ',';
}
ids += deviceIds[i];
}
var url = '/api/devices?deviceIds=' + ids;
$http.get(url, null).then(function success(response) {
var devices = response.data;
devices.sort(function (device1, device2) {
var id1 = device1.id.id;
var id2 = device2.id.id;
var index1 = deviceIds.indexOf(id1);
var index2 = deviceIds.indexOf(id2);
return index1 - index2;
});
deferred.resolve(devices);
}, function fail(response) {
deferred.reject(response.data);
});
return deferred.promise;
}
function fetchAliasDeviceByNameFilter(deviceNameFilter, limit) {
var deferred = $q.defer();
var user = userService.getCurrentUser();
var promise;
var pageLink = {limit: limit, textSearch: deviceNameFilter};
if (user.authority === 'CUSTOMER_USER') {
var customerId = user.customerId;
promise = getCustomerDevices(customerId, pageLink);
} else {
promise = getTenantDevices(pageLink);
}
promise.then(
function success(result) {
if (result.data && result.data.length > 0) {
deferred.resolve(result.data);
} else {
deferred.resolve(null);
}
},
function fail() {
deferred.resolve(null);
}
);
return deferred.promise;
}
function deviceToDeviceInfo(device) {
return { name: device.name, id: device.id.id };
}
function devicesToDevicesInfo(devices) {
var devicesInfo = [];
for (var d in devices) {
devicesInfo.push(deviceToDeviceInfo(devices[d]));
}
return devicesInfo;
}
function processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred) {
if (index < aliasIds.length) {
var aliasId = aliasIds[index];
var deviceAlias = deviceAliases[aliasId];
var alias = deviceAlias.alias;
if (!deviceAlias.deviceFilter) {
getDevice(deviceAlias.deviceId).then(
function success(device) {
var resolvedAlias = {alias: alias, deviceId: device.id.id};
resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.deviceAliasesInfo[aliasId] = [
deviceToDeviceInfo(device)
];
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
},
function fail() {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
);
} else {
var deviceFilter = deviceAlias.deviceFilter;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
fetchAliasDeviceByNameFilter(deviceNameFilter, 100).then(
function(devices) {
if (devices && devices != null) {
var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
} else {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
});
} else {
var deviceList = deviceFilter.deviceList;
getDevices(deviceList).then(
function success(devices) {
if (devices && devices.length > 0) {
var resolvedAlias = {alias: alias, deviceId: devices[0].id.id};
resolution.aliasesInfo.deviceAliases[aliasId] = resolvedAlias;
resolution.aliasesInfo.deviceAliasesInfo[aliasId] = devicesToDevicesInfo(devices);
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
} else {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
},
function fail() {
if (!resolution.error) {
resolution.error = 'dashboard.invalid-aliases-config';
}
index++;
processDeviceAlias(index, aliasIds, deviceAliases, resolution, deferred);
}
);
}
}
} else {
deferred.resolve(resolution);
}
}
function processDeviceAliases(deviceAliases) {
var deferred = $q.defer();
var resolution = {
aliasesInfo: {
deviceAliases: {},
deviceAliasesInfo: {}
}
};
var aliasIds = [];
if (deviceAliases) {
for (var aliasId in deviceAliases) {
aliasIds.push(aliasId);
}
}
processDeviceAlias(0, aliasIds, deviceAliases, resolution, deferred);
return deferred.promise;
}
function checkDeviceAlias(deviceAlias) {
var deferred = $q.defer();
var deviceFilter;
if (deviceAlias.deviceId) {
deviceFilter = {
useFilter: false,
deviceNameFilter: '',
deviceList: [deviceAlias.deviceId]
}
} else {
deviceFilter = deviceAlias.deviceFilter;
}
var promise;
if (deviceFilter.useFilter) {
var deviceNameFilter = deviceFilter.deviceNameFilter;
promise = fetchAliasDeviceByNameFilter(deviceNameFilter, 1);
} else {
var deviceList = deviceFilter.deviceList;
promise = getDevices(deviceList);
}
promise.then(
function success(devices) {
if (devices && devices.length > 0) {
deferred.resolve(true);
} else {
deferred.resolve(false);
}
},
function fail() {
deferred.resolve(false);
}
);
return deferred.promise;
}
function saveDevice(device) { function saveDevice(device) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device'; var url = '/api/device';
@ -212,7 +409,7 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
return deferred.promise; return deferred.promise;
} }
function processDeviceAttributes(attributes, query, deferred, successCallback, update) { function processDeviceAttributes(attributes, query, deferred, successCallback, update, apply) {
attributes = $filter('orderBy')(attributes, query.order); attributes = $filter('orderBy')(attributes, query.order);
if (query.search != null) { if (query.search != null) {
attributes = $filter('filter')(attributes, {key: query.search}); attributes = $filter('filter')(attributes, {key: query.search});
@ -222,7 +419,7 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
} }
var startIndex = query.limit * (query.page - 1); var startIndex = query.limit * (query.page - 1);
responseData.data = attributes.slice(startIndex, startIndex + query.limit); responseData.data = attributes.slice(startIndex, startIndex + query.limit);
successCallback(responseData, update); successCallback(responseData, update, apply);
if (deferred) { if (deferred) {
deferred.resolve(); deferred.resolve();
} }
@ -236,13 +433,13 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
if (das.attributes) { if (das.attributes) {
processDeviceAttributes(das.attributes, query, deferred, successCallback); processDeviceAttributes(das.attributes, query, deferred, successCallback);
das.subscriptionCallback = function(attributes) { das.subscriptionCallback = function(attributes) {
processDeviceAttributes(attributes, query, null, successCallback, true); processDeviceAttributes(attributes, query, null, successCallback, true, true);
} }
} else { } else {
das.subscriptionCallback = function(attributes) { das.subscriptionCallback = function(attributes) {
processDeviceAttributes(attributes, query, deferred, successCallback); processDeviceAttributes(attributes, query, deferred, successCallback, false, true);
das.subscriptionCallback = function(attributes) { das.subscriptionCallback = function(attributes) {
processDeviceAttributes(attributes, query, null, successCallback, true); processDeviceAttributes(attributes, query, null, successCallback, true, true);
} }
} }
} }
@ -304,7 +501,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
subscriptionCommand: subscriptionCommand, subscriptionCommand: subscriptionCommand,
type: type, type: type,
onData: function (data) { onData: function (data) {
onSubscriptionData(data, subscriptionId); if (data.data) {
onSubscriptionData(data.data, subscriptionId);
}
} }
}; };
deviceAttributesSubscription = { deviceAttributesSubscription = {

28
ui/src/app/api/telemetry-websocket.service.js

@ -131,14 +131,38 @@ function TelemetryWebsocketService($rootScope, $websocket, $timeout, $window, ty
var data = angular.fromJson(message.data); var data = angular.fromJson(message.data);
if (data.subscriptionId) { if (data.subscriptionId) {
var subscriber = subscribers[data.subscriptionId]; var subscriber = subscribers[data.subscriptionId];
if (subscriber && data.data) { if (subscriber && data) {
subscriber.onData(data.data); var keys = fetchKeys(subscriber);
if (!data.data) {
data.data = {};
}
for (var k in keys) {
var key = keys[k];
if (!data.data[key]) {
data.data[key] = [];
}
}
subscriber.onData(data);
} }
} }
} }
checkToClose(); checkToClose();
} }
function fetchKeys(subscriber) {
var command;
if (angular.isDefined(subscriber.subscriptionCommand)) {
command = subscriber.subscriptionCommand;
} else {
command = subscriber.historyCommand;
}
if (command && command.keys && command.keys.length > 0) {
return command.keys.split(",");
} else {
return [];
}
}
function nextCmdId () { function nextCmdId () {
lastCmdId++; lastCmdId++;
return lastCmdId; return lastCmdId;

330
ui/src/app/api/time.service.js

@ -0,0 +1,330 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default angular.module('thingsboard.api.time', [])
.factory('timeService', TimeService)
.name;
const SECOND = 1000;
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const MIN_INTERVAL = SECOND;
const MAX_INTERVAL = 365 * 20 * DAY;
const MIN_LIMIT = 10;
const AVG_LIMIT = 200;
const MAX_LIMIT = 500;
/*@ngInject*/
function TimeService($translate, types) {
var predefIntervals = [
{
name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'),
value: 1 * SECOND
},
{
name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'),
value: 5 * SECOND
},
{
name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
value: 10 * SECOND
},
{
name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'),
value: 15 * SECOND
},
{
name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
value: 30 * SECOND
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
value: 1 * MINUTE
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
value: 2 * MINUTE
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
value: 5 * MINUTE
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
value: 10 * MINUTE
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'),
value: 15 * MINUTE
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
value: 30 * MINUTE
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
value: 1 * HOUR
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
value: 2 * HOUR
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'),
value: 5 * HOUR
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
value: 10 * HOUR
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'),
value: 12 * HOUR
},
{
name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
value: 1 * DAY
},
{
name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
value: 7 * DAY
},
{
name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
value: 30 * DAY
}
];
var service = {
minIntervalLimit: minIntervalLimit,
maxIntervalLimit: maxIntervalLimit,
boundMinInterval: boundMinInterval,
boundMaxInterval: boundMaxInterval,
getIntervals: getIntervals,
matchesExistingInterval: matchesExistingInterval,
boundToPredefinedInterval: boundToPredefinedInterval,
defaultTimewindow: defaultTimewindow,
toHistoryTimewindow: toHistoryTimewindow,
createSubscriptionTimewindow: createSubscriptionTimewindow,
avgAggregationLimit: function () {
return AVG_LIMIT;
}
}
return service;
function minIntervalLimit(timewindow) {
var min = timewindow / MAX_LIMIT;
return boundMinInterval(min);
}
function avgInterval(timewindow) {
var avg = timewindow / AVG_LIMIT;
return boundMinInterval(avg);
}
function maxIntervalLimit(timewindow) {
var max = timewindow / MIN_LIMIT;
return boundMaxInterval(max);
}
function boundMinInterval(min) {
return toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL);
}
function boundMaxInterval(max) {
return toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL);
}
function toBound(value, min, max, defValue) {
if (angular.isDefined(value)) {
value = Math.max(value, min);
value = Math.min(value, max);
return value;
} else {
return defValue;
}
}
function getIntervals(min, max) {
min = boundMinInterval(min);
max = boundMaxInterval(max);
var intervals = [];
for (var i in predefIntervals) {
var interval = predefIntervals[i];
if (interval.value >= min && interval.value <= max) {
intervals.push(interval);
}
}
return intervals;
}
function matchesExistingInterval(min, max, intervalMs) {
var intervals = getIntervals(min, max);
for (var i in intervals) {
var interval = intervals[i];
if (intervalMs === interval.value) {
return true;
}
}
return false;
}
function boundToPredefinedInterval(min, max, intervalMs) {
var intervals = getIntervals(min, max);
var minDelta = MAX_INTERVAL;
var boundedInterval = intervalMs || min;
var matchedInterval;
for (var i in intervals) {
var interval = intervals[i];
var delta = Math.abs(interval.value - boundedInterval);
if (delta < minDelta) {
matchedInterval = interval;
minDelta = delta;
}
}
boundedInterval = matchedInterval.value;
return boundedInterval;
}
function defaultTimewindow() {
var currentTime = (new Date).getTime();
var timewindow = {
displayValue: "",
selectedTab: 0,
realtime: {
interval: SECOND,
timewindowMs: MINUTE // 1 min by default
},
history: {
historyType: 0,
interval: SECOND,
timewindowMs: MINUTE, // 1 min by default
fixedTimewindow: {
startTimeMs: currentTime - DAY, // 1 day by default
endTimeMs: currentTime
}
},
aggregation: {
type: types.aggregation.avg.value,
limit: AVG_LIMIT
}
}
return timewindow;
}
function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs) {
var interval = 0;
if (timewindow.history) {
interval = timewindow.history.interval;
} else if (timewindow.realtime) {
interval = timewindow.realtime.interval;
}
var historyTimewindow = {
history: {
fixedTimewindow: {
startTimeMs: startTimeMs,
endTimeMs: endTimeMs
},
interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval)
},
aggregation: {
}
}
if (timewindow.aggregation) {
historyTimewindow.aggregation.type = timewindow.aggregation.type || types.aggregation.avg.value;
} else {
historyTimewindow.aggregation.type = types.aggregation.avg.value;
}
return historyTimewindow;
}
function createSubscriptionTimewindow(timewindow, stDiff) {
var subscriptionTimewindow = {
fixedWindow: null,
realtimeWindowMs: null,
aggregation: {
interval: SECOND,
limit: AVG_LIMIT,
type: types.aggregation.avg.value
}
};
var aggTimewindow = 0;
if (angular.isDefined(timewindow.aggregation)) {
subscriptionTimewindow.aggregation = {
type: timewindow.aggregation.type || types.aggregation.avg.value,
limit: timewindow.aggregation.limit || AVG_LIMIT
};
}
if (angular.isDefined(timewindow.realtime)) {
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs;
subscriptionTimewindow.aggregation.interval =
boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval);
subscriptionTimewindow.startTs = (new Date).getTime() + stDiff - subscriptionTimewindow.realtimeWindowMs;
var startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval;
aggTimewindow = subscriptionTimewindow.realtimeWindowMs;
if (startDiff) {
subscriptionTimewindow.startTs -= startDiff;
aggTimewindow += subscriptionTimewindow.aggregation.interval;
}
} else if (angular.isDefined(timewindow.history)) {
if (angular.isDefined(timewindow.history.timewindowMs)) {
var currentTime = (new Date).getTime();
subscriptionTimewindow.fixedWindow = {
startTimeMs: currentTime - timewindow.history.timewindowMs,
endTimeMs: currentTime
}
aggTimewindow = timewindow.history.timewindowMs;
} else {
subscriptionTimewindow.fixedWindow = {
startTimeMs: timewindow.history.fixedTimewindow.startTimeMs,
endTimeMs: timewindow.history.fixedTimewindow.endTimeMs
}
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs;
}
subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs;
subscriptionTimewindow.aggregation.interval = boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval);
}
var aggregation = subscriptionTimewindow.aggregation;
aggregation.timeWindow = aggTimewindow;
if (aggregation.type !== types.aggregation.none.value) {
aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval);
}
return subscriptionTimewindow;
}
function boundIntervalToTimewindow(timewindow, intervalMs) {
var min = minIntervalLimit(timewindow);
var max = maxIntervalLimit(timewindow);
if (intervalMs) {
return toBound(intervalMs, min, max, intervalMs);
} else {
return boundToPredefinedInterval(min, max, avgInterval(timewindow));
}
}
}

62
ui/src/app/api/user.service.js

@ -22,8 +22,9 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
.name; .name;
/*@ngInject*/ /*@ngInject*/
function UserService($http, $q, $rootScope, store, jwtHelper, $translate) { function UserService($http, $q, $rootScope, store, jwtHelper, $translate, $state) {
var currentUser = null, var currentUser = null,
currentUserDetails = null,
userLoaded = false; userLoaded = false;
var refreshTokenQueue = []; var refreshTokenQueue = [];
@ -47,6 +48,8 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
refreshJwtToken: refreshJwtToken, refreshJwtToken: refreshJwtToken,
refreshTokenPending: refreshTokenPending, refreshTokenPending: refreshTokenPending,
updateAuthorizationHeader: updateAuthorizationHeader, updateAuthorizationHeader: updateAuthorizationHeader,
gotoDefaultPlace: gotoDefaultPlace,
forceDefaultPlace: forceDefaultPlace,
logout: logout logout: logout
} }
@ -86,6 +89,7 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) { function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
currentUser = null; currentUser = null;
currentUserDetails = null;
if (!jwtToken) { if (!jwtToken) {
clearTokenData(); clearTokenData();
if (notify) { if (notify) {
@ -222,7 +226,25 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
} else if (currentUser) { } else if (currentUser) {
currentUser.authority = "ANONYMOUS"; currentUser.authority = "ANONYMOUS";
} }
deferred.resolve(); if (currentUser.userId) {
getUser(currentUser.userId).then(
function success(user) {
currentUserDetails = user;
if (currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardFullscreen) {
$rootScope.forceFullscreen = currentUserDetails.additionalInfo.defaultDashboardFullscreen === true;
} else {
$rootScope.forceFullscreen = false;
}
deferred.resolve();
},
function fail() {
deferred.reject();
}
)
} else {
deferred.reject();
}
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
}); });
@ -331,4 +353,40 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
return deferred.promise; return deferred.promise;
} }
function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) {
if (currentUser.authority === 'CUSTOMER_USER') {
if (currentUserDetails &&
currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardId) {
if ($rootScope.forceFullscreen) {
if (to.name === 'home.profile') {
return false;
} else if (to.name !== 'home.dashboards.dashboard' && params.dashboardId !== currentUserDetails.additionalInfo.defaultDashboardId) {
return true;
}
}
}
}
}
return false;
}
function gotoDefaultPlace(params) {
if (currentUser && isAuthenticated()) {
var place = 'home.links';
if (currentUser.authority === 'CUSTOMER_USER') {
if (currentUserDetails &&
currentUserDetails.additionalInfo &&
currentUserDetails.additionalInfo.defaultDashboardId) {
place = 'home.dashboards.dashboard';
params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
}
}
$state.go(place, params);
} else {
$state.go('login', params);
}
}
} }

176
ui/src/app/api/widget.service.js

@ -19,9 +19,10 @@ import tinycolor from 'tinycolor2';
import thinsboardLedLight from '../components/led-light.directive'; import thinsboardLedLight from '../components/led-light.directive';
import TbFlot from '../widget/lib/flot-widget';
import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge'; import TbAnalogueRadialGauge from '../widget/lib/analogue-radial-gauge';
import TbDigitalGauge from '../widget/lib/digital-gauge'; import TbCanvasDigitalGauge from '../widget/lib/canvas-digital-gauge';
import TbMapWidget from '../widget/lib/map-widget'; import TbMapWidget from '../widget/lib/map-widget';
import 'oclazyload'; import 'oclazyload';
@ -38,13 +39,15 @@ export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thinsboa
function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, types, utils) { function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, types, utils) {
$window.$ = $; $window.$ = $;
$window.jQuery = $;
$window.moment = moment; $window.moment = moment;
$window.tinycolor = tinycolor; $window.tinycolor = tinycolor;
$window.lazyLoad = $ocLazyLoad; $window.lazyLoad = $ocLazyLoad;
$window.TbFlot = TbFlot;
$window.TbAnalogueLinearGauge = TbAnalogueLinearGauge; $window.TbAnalogueLinearGauge = TbAnalogueLinearGauge;
$window.TbAnalogueRadialGauge = TbAnalogueRadialGauge; $window.TbAnalogueRadialGauge = TbAnalogueRadialGauge;
$window.TbDigitalGauge = TbDigitalGauge; $window.TbCanvasDigitalGauge = TbCanvasDigitalGauge;
$window.TbMapWidget = TbMapWidget; $window.TbMapWidget = TbMapWidget;
$window.cssjs = cssjs; $window.cssjs = cssjs;
@ -57,6 +60,8 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
var editingWidgetType; var editingWidgetType;
var widgetsInfoInMemoryCache = {}; var widgetsInfoInMemoryCache = {};
var widgetsTypeFunctionsInMemoryCache = {};
var widgetsInfoFetchQueue = {};
var allWidgetsBundles = undefined; var allWidgetsBundles = undefined;
var systemWidgetsBundles = undefined; var systemWidgetsBundles = undefined;
@ -83,6 +88,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
deleteWidgetsBundle: deleteWidgetsBundle, deleteWidgetsBundle: deleteWidgetsBundle,
getBundleWidgetTypes: getBundleWidgetTypes, getBundleWidgetTypes: getBundleWidgetTypes,
getWidgetInfo: getWidgetInfo, getWidgetInfo: getWidgetInfo,
getWidgetTypeFunction: getWidgetTypeFunction,
getInstantWidgetInfo: getInstantWidgetInfo, getInstantWidgetInfo: getInstantWidgetInfo,
deleteWidgetType: deleteWidgetType, deleteWidgetType: deleteWidgetType,
saveWidgetType: saveWidgetType, saveWidgetType: saveWidgetType,
@ -123,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
resources: [], resources: [],
templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>', templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>',
templateCss: '', templateCss: '',
controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};', controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n', settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n', dataKeySettingsSchema: '{}\n',
defaultConfig: '{\n' + defaultConfig: '{\n' +
@ -141,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
resources: [], resources: [],
templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>', templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
templateCss: '', templateCss: '',
controllerScript: 'fns.init = function(containerElement, settings, datasources,\n data) {}\n\n\nfns.redraw = function(containerElement, width, height, data) {};', controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n', settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n', dataKeySettingsSchema: '{}\n',
defaultConfig: '{\n' + defaultConfig: '{\n' +
@ -228,19 +234,34 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
/** Cache functions **/ /** Cache functions **/
function createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem) {
return (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
}
function getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) { function getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) {
var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias; var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
return widgetsInfoInMemoryCache[key]; return widgetsInfoInMemoryCache[key];
} }
function getWidgetTypeFunctionFromCache(bundleAlias, widgetTypeAlias, isSystem) {
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
return widgetsTypeFunctionsInMemoryCache[key];
}
function putWidgetInfoToCache(widgetInfo, bundleAlias, widgetTypeAlias, isSystem) { function putWidgetInfoToCache(widgetInfo, bundleAlias, widgetTypeAlias, isSystem) {
var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias; var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
widgetsInfoInMemoryCache[key] = widgetInfo; widgetsInfoInMemoryCache[key] = widgetInfo;
} }
function putWidgetTypeFunctionToCache(widgetTypeFunction, bundleAlias, widgetTypeAlias, isSystem) {
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
widgetsTypeFunctionsInMemoryCache[key] = widgetTypeFunction;
}
function deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) { function deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) {
var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias; var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
delete widgetsInfoInMemoryCache[key]; delete widgetsInfoInMemoryCache[key];
delete widgetsTypeFunctionsInMemoryCache[key];
} }
function deleteWidgetsBundleFromCache(bundleAlias, isSystem) { function deleteWidgetsBundleFromCache(bundleAlias, isSystem) {
@ -248,6 +269,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
for (var cacheKey in widgetsInfoInMemoryCache) { for (var cacheKey in widgetsInfoInMemoryCache) {
if (cacheKey.startsWith(key)) { if (cacheKey.startsWith(key)) {
delete widgetsInfoInMemoryCache[cacheKey]; delete widgetsInfoInMemoryCache[cacheKey];
delete widgetsTypeFunctionsInMemoryCache[cacheKey];
} }
} }
} }
@ -456,6 +478,16 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
} }
} }
function resolveWidgetsInfoFetchQueue(key, widgetInfo) {
var fetchQueue = widgetsInfoFetchQueue[key];
if (fetchQueue) {
for (var q in fetchQueue) {
fetchQueue[q].resolve(widgetInfo);
}
delete widgetsInfoFetchQueue[key];
}
}
function getWidgetInfo(bundleAlias, widgetTypeAlias, isSystem) { function getWidgetInfo(bundleAlias, widgetTypeAlias, isSystem) {
var deferred = $q.defer(); var deferred = $q.defer();
var widgetInfo = getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem); var widgetInfo = getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
@ -465,32 +497,130 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if ($rootScope.widgetEditMode) { if ($rootScope.widgetEditMode) {
loadWidget(editingWidgetType, bundleAlias, isSystem, deferred); loadWidget(editingWidgetType, bundleAlias, isSystem, deferred);
} else { } else {
getWidgetType(bundleAlias, widgetTypeAlias, isSystem).then( var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
function success(widgetType) { var fetchQueue = widgetsInfoFetchQueue[key];
loadWidget(widgetType, bundleAlias, isSystem, deferred); if (fetchQueue) {
}, function fail() { fetchQueue.push(deferred);
deferred.resolve(missingWidgetType); } else {
} fetchQueue = [];
); widgetsInfoFetchQueue[key] = fetchQueue;
getWidgetType(bundleAlias, widgetTypeAlias, isSystem).then(
function success(widgetType) {
loadWidget(widgetType, bundleAlias, isSystem, deferred);
}, function fail() {
deferred.resolve(missingWidgetType);
resolveWidgetsInfoFetchQueue(key, missingWidgetType);
}
);
}
} }
} }
return deferred.promise; return deferred.promise;
} }
function getWidgetTypeFunction(bundleAlias, widgetTypeAlias, isSystem) {
var widgetType = getWidgetTypeFunctionFromCache(bundleAlias, widgetTypeAlias, isSystem);
return widgetType;
}
function createWidgetTypeFunction(widgetInfo, name) {
var widgetTypeFunctionBody = 'return function '+ name +' (ctx) {\n' +
' var self = this;\n' +
' self.ctx = ctx;\n\n'; /*+
' self.onInit = function() {\n\n' +
' }\n\n' +
' self.onDataUpdated = function() {\n\n' +
' }\n\n' +
' self.onResize = function() {\n\n' +
' }\n\n' +
' self.onEditModeChanged = function() {\n\n' +
' }\n\n' +
' self.onMobileModeChanged = function() {\n\n' +
' }\n\n' +
' self.getSettingsSchema = function() {\n\n' +
' }\n\n' +
' self.getDataKeySettingsSchema = function() {\n\n' +
' }\n\n' +
' self.onDestroy = function() {\n\n' +
' }\n\n' +
'}';*/
widgetTypeFunctionBody += widgetInfo.controllerScript;
widgetTypeFunctionBody += '\n};\n';
try {
var widgetTypeFunction = new Function(widgetTypeFunctionBody);
var widgetType = widgetTypeFunction.apply(this);
var widgetTypeInstance = new widgetType();
var result = {
widgetTypeFunction: widgetType
};
if (angular.isFunction(widgetTypeInstance.getSettingsSchema)) {
result.settingsSchema = widgetTypeInstance.getSettingsSchema();
}
if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
}
return result;
} catch (e) {
utils.processWidgetException(e);
throw e;
}
}
function processWidgetLoadError(errorMessages, cacheKey, deferred) {
var widgetInfo = angular.copy(errorWidgetType);
for (var e in errorMessages) {
var error = errorMessages[e];
widgetInfo.templateHtml += '<div class="tb-widget-error-msg">' + error + '</div>';
}
widgetInfo.templateHtml += '</div>';
deferred.resolve(widgetInfo);
resolveWidgetsInfoFetchQueue(cacheKey, widgetInfo);
}
function loadWidget(widgetType, bundleAlias, isSystem, deferred) { function loadWidget(widgetType, bundleAlias, isSystem, deferred) {
var widgetInfo = toWidgetInfo(widgetType); var widgetInfo = toWidgetInfo(widgetType);
var key = createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem);
loadWidgetResources(widgetInfo, bundleAlias, isSystem).then( loadWidgetResources(widgetInfo, bundleAlias, isSystem).then(
function success() { function success() {
putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); var widgetType = null;
deferred.resolve(widgetInfo); try {
}, function fail(errorMessages) { widgetType = createWidgetTypeFunction(widgetInfo, key);
widgetInfo = angular.copy(errorWidgetType); } catch (e) {
for (var e in errorMessages) { var details = utils.parseException(e);
var error = errorMessages[e]; var errorMessage = 'Failed to compile widget script. \n Error: ' + details.message;
widgetInfo.templateHtml += '<div class="tb-widget-error-msg">' + error + '</div>'; processWidgetLoadError([errorMessage], key, deferred);
} }
widgetInfo.templateHtml += '</div>'; if (widgetType) {
deferred.resolve(widgetInfo); if (widgetType.settingsSchema) {
widgetInfo.typeSettingsSchema = widgetType.settingsSchema;
}
if (widgetType.dataKeySettingsSchema) {
widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
}
putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
deferred.resolve(widgetInfo);
resolveWidgetsInfoFetchQueue(key, widgetInfo);
}
}, function fail(errorMessages) {
processWidgetLoadError(errorMessages, key, deferred);
} }
); );
} }

23
ui/src/app/app.config.js

@ -33,29 +33,30 @@ export default function AppConfig($provide,
$mdThemingProvider, $mdThemingProvider,
$httpProvider, $httpProvider,
$translateProvider, $translateProvider,
storeProvider) { storeProvider,
locales) {
injectTapEventPlugin(); injectTapEventPlugin();
$locationProvider.html5Mode(true); $locationProvider.html5Mode(true);
$urlRouterProvider.otherwise('/home'); //$urlRouterProvider.otherwise('/home');
storeProvider.setCaching(false); storeProvider.setCaching(false);
$translateProvider.useStaticFilesLoader({
prefix: 'static/locale/',// path to translations files
suffix: '.json'// suffix, currently- extension of the translations
});
$translateProvider.useSanitizeValueStrategy('sanitize'); $translateProvider.useSanitizeValueStrategy('sanitize');
$translateProvider.preferredLanguage('en_US'); $translateProvider.preferredLanguage('en_US');
$translateProvider.useLocalStorage(); $translateProvider.useLocalStorage();
$translateProvider.useMissingTranslationHandlerLog(); $translateProvider.useMissingTranslationHandlerLog();
$translateProvider.addInterpolation('$translateMessageFormatInterpolation'); $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
for (var langKey in locales) {
var translationTable = locales[langKey];
$translateProvider.translations(langKey, translationTable);
}
$httpProvider.interceptors.push('globalInterceptor'); $httpProvider.interceptors.push('globalInterceptor');
$provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate, $injector) { $provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate/*, $injector*/) {
return function (exception, cause) { return function (exception, cause) {
var rootScope = $injector.get("$rootScope"); /* var rootScope = $injector.get("$rootScope");
var $window = $injector.get("$window"); var $window = $injector.get("$window");
var utils = $injector.get("utils"); var utils = $injector.get("utils");
if (rootScope.widgetEditMode) { if (rootScope.widgetEditMode) {
@ -63,7 +64,7 @@ export default function AppConfig($provide,
var data = utils.parseException(exception); var data = utils.parseException(exception);
parentScope.$emit('widgetException', data); parentScope.$emit('widgetException', data);
parentScope.$apply(); parentScope.$apply();
} }*/
$delegate(exception, cause); $delegate(exception, cause);
}; };
}]); }]);
@ -141,7 +142,7 @@ export default function AppConfig($provide,
.backgroundPalette('tb-primary'); .backgroundPalette('tb-primary');
$mdThemingProvider.setDefaultTheme('default'); $mdThemingProvider.setDefaultTheme('default');
$mdThemingProvider.alwaysWatchTheme(true); //$mdThemingProvider.alwaysWatchTheme(true);
} }
} }

7
ui/src/app/app.js

@ -44,11 +44,14 @@ import 'react-schema-form';
import react from 'ngreact'; import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min'; import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import thingsboardLocales from './locale/locale.constant';
import thingsboardLogin from './login'; import thingsboardLogin from './login';
import thingsboardDialogs from './components/datakey-config-dialog.controller'; import thingsboardDialogs from './components/datakey-config-dialog.controller';
import thingsboardMenu from './services/menu.service'; import thingsboardMenu from './services/menu.service';
import thingsboardRaf from './common/raf.provider';
import thingsboardUtils from './common/utils.service'; import thingsboardUtils from './common/utils.service';
import thingsboardTypes from './common/types.constant'; import thingsboardTypes from './common/types.constant';
import thingsboardApiTime from './api/time.service';
import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
import thingsboardHelp from './help/help.directive'; import thingsboardHelp from './help/help.directive';
import thingsboardToast from './services/toast'; import thingsboardToast from './services/toast';
@ -57,6 +60,7 @@ import thingsboardApiLogin from './api/login.service';
import thingsboardApiDevice from './api/device.service'; import thingsboardApiDevice from './api/device.service';
import thingsboardApiUser from './api/user.service'; import thingsboardApiUser from './api/user.service';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css'; import 'font-awesome/css/font-awesome.min.css';
import 'angular-material/angular-material.min.css'; import 'angular-material/angular-material.min.css';
import 'angular-material-icons/angular-material-icons.css'; import 'angular-material-icons/angular-material-icons.css';
@ -91,11 +95,14 @@ angular.module('thingsboard', [
'ngclipboard', 'ngclipboard',
react.name, react.name,
'flow', 'flow',
thingsboardLocales,
thingsboardLogin, thingsboardLogin,
thingsboardDialogs, thingsboardDialogs,
thingsboardMenu, thingsboardMenu,
thingsboardRaf,
thingsboardUtils, thingsboardUtils,
thingsboardTypes, thingsboardTypes,
thingsboardApiTime,
thingsboardKeyboardShortcut, thingsboardKeyboardShortcut,
thingsboardHelp, thingsboardHelp,
thingsboardToast, thingsboardToast,

55
ui/src/app/app.run.js

@ -36,6 +36,8 @@ export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $fi
initWatchers(); initWatchers();
checkCurrentState();
function initWatchers() { function initWatchers() {
$rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) { $rootScope.unauthenticatedHandle = $rootScope.$on('unauthenticated', function (event, doLogout) {
if (doLogout) { if (doLogout) {
@ -56,22 +58,27 @@ export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $fi
$rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) { $rootScope.stateChangeStartHandle = $rootScope.$on('$stateChangeStart', function (evt, to, params) {
if (userService.isUserLoaded() === true) { if (userService.isUserLoaded() === true) {
if (userService.isAuthenticated()) { if (userService.isAuthenticated()) {
var authority = userService.getAuthority(); if (userService.forceDefaultPlace(to, params)) {
if (to.module === 'public') {
evt.preventDefault();
$state.go('home', params);
} else if (angular.isDefined(to.auth) &&
to.auth.indexOf(authority) === -1) {
evt.preventDefault(); evt.preventDefault();
showForbiddenDialog(); gotoDefaultPlace(params);
} else if (to.redirectTo) { } else {
evt.preventDefault(); var authority = userService.getAuthority();
$state.go(to.redirectTo, params) if (to.module === 'public') {
evt.preventDefault();
gotoDefaultPlace(params);
} else if (angular.isDefined(to.auth) &&
to.auth.indexOf(authority) === -1) {
evt.preventDefault();
showForbiddenDialog();
} else if (to.redirectTo) {
evt.preventDefault();
$state.go(to.redirectTo, params)
}
} }
} else { } else {
if (to.module === 'private') { if (to.module === 'private') {
evt.preventDefault(); evt.preventDefault();
if (to.url === '/home') { if (to.url === '/home' || to.url === '/') {
$state.go('login', params); $state.go('login', params);
} else { } else {
showUnauthorizedDialog(); showUnauthorizedDialog();
@ -80,6 +87,9 @@ export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $fi
} }
} else { } else {
evt.preventDefault(); evt.preventDefault();
if ($rootScope.userLoadedHandle) {
$rootScope.userLoadedHandle();
}
$rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () { $rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
$rootScope.userLoadedHandle(); $rootScope.userLoadedHandle();
$state.go(to.name, params); $state.go(to.name, params);
@ -102,23 +112,26 @@ export default function AppRun($rootScope, $window, $log, $state, $mdDialog, $fi
function checkCurrentState() { function checkCurrentState() {
if (userService.isUserLoaded() === true) { if (userService.isUserLoaded() === true) {
var module = $state.$current.module;
if (userService.isAuthenticated()) { if (userService.isAuthenticated()) {
if ($state.$current.module === 'public') { gotoDefaultPlace();
$state.go('home');
}
} else { } else {
if (angular.isUndefined(module) || !module) { $state.go('login');
//$state.go('login');
} else if ($state.$current.module === 'private') {
showUnauthorizedDialog();
}
} }
} else { } else {
showUnauthorizedDialog(); if ($rootScope.userLoadedHandle) {
$rootScope.userLoadedHandle();
}
$rootScope.userLoadedHandle = $rootScope.$on('userLoaded', function () {
$rootScope.userLoadedHandle();
checkCurrentState();
});
} }
} }
function gotoDefaultPlace(params) {
userService.gotoDefaultPlace(params);
}
function showUnauthorizedDialog() { function showUnauthorizedDialog() {
if (unauthorizedDialog === null) { if (unauthorizedDialog === null) {
$translate(['access.unauthorized-access', $translate(['access.unauthorized-access',

50
ui/src/app/common/raf.provider.js

@ -0,0 +1,50 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default angular.module('thingsboard.raf', [])
.provider('tbRaf', TbRAFProvider)
.name;
function TbRAFProvider() {
/*@ngInject*/
this.$get = function($window, $timeout) {
var requestAnimationFrame = $window.requestAnimationFrame ||
$window.webkitRequestAnimationFrame;
var cancelAnimationFrame = $window.cancelAnimationFrame ||
$window.webkitCancelAnimationFrame ||
$window.webkitCancelRequestAnimationFrame;
var rafSupported = !!requestAnimationFrame;
var raf = rafSupported
? function(fn) {
var id = requestAnimationFrame(fn);
return function() {
cancelAnimationFrame(id);
};
}
: function(fn) {
var timer = $timeout(fn, 16.66, false);
return function() {
$timeout.cancel(timer);
};
};
raf.supported = rafSupported;
return raf;
};
}

44
ui/src/app/common/types.constant.js

@ -33,6 +33,50 @@ export default angular.module('thingsboard.types', [])
id: { id: {
nullUid: "13814000-1dd2-11b2-8080-808080808080", nullUid: "13814000-1dd2-11b2-8080-808080808080",
}, },
aggregation: {
min: {
value: "MIN",
name: "aggregation.min"
},
max: {
value: "MAX",
name: "aggregation.max"
},
avg: {
value: "AVG",
name: "aggregation.avg"
},
sum: {
value: "SUM",
name: "aggregation.sum"
},
count: {
value: "COUNT",
name: "aggregation.count"
},
none: {
value: "NONE",
name: "aggregation.none"
}
},
position: {
top: {
value: "top",
name: "position.top"
},
bottom: {
value: "bottom",
name: "position.bottom"
},
left: {
value: "left",
name: "position.left"
},
right: {
value: "right",
name: "position.right"
}
},
datasourceType: { datasourceType: {
function: "function", function: "function",
device: "device" device: "device"

20
ui/src/app/common/utils.service.js

@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function Utils($mdColorPalette, types) { function Utils($mdColorPalette, $rootScope, $window, types) {
var predefinedFunctions = {}, var predefinedFunctions = {},
predefinedFunctionsList = [], predefinedFunctionsList = [],
@ -102,6 +102,7 @@ function Utils($mdColorPalette, types) {
genMaterialColor: genMaterialColor, genMaterialColor: genMaterialColor,
objectHashCode: objectHashCode, objectHashCode: objectHashCode,
parseException: parseException, parseException: parseException,
processWidgetException: processWidgetException,
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty, isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities filterSearchTextEntities: filterSearchTextEntities
} }
@ -147,7 +148,7 @@ function Utils($mdColorPalette, types) {
return hash; return hash;
} }
function parseException(exception) { function parseException(exception, lineOffset) {
var data = {}; var data = {};
if (exception) { if (exception) {
if (angular.isString(exception) || exception instanceof String) { if (angular.isString(exception) || exception instanceof String) {
@ -170,7 +171,10 @@ function Utils($mdColorPalette, types) {
var lineInfoRegexp = /(.*<anonymous>):(\d*)(:)?(\d*)?/g; var lineInfoRegexp = /(.*<anonymous>):(\d*)(:)?(\d*)?/g;
var lineInfoGroups = lineInfoRegexp.exec(exception.stack); var lineInfoGroups = lineInfoRegexp.exec(exception.stack);
if (lineInfoGroups != null && lineInfoGroups.length >= 3) { if (lineInfoGroups != null && lineInfoGroups.length >= 3) {
data.lineNumber = lineInfoGroups[2] - 2; if (angular.isUndefined(lineOffset)) {
lineOffset = -2;
}
data.lineNumber = Number(lineInfoGroups[2]) + lineOffset;
if (lineInfoGroups.length >= 5) { if (lineInfoGroups.length >= 5) {
data.columnNumber = lineInfoGroups[4]; data.columnNumber = lineInfoGroups[4];
} }
@ -181,6 +185,16 @@ function Utils($mdColorPalette, types) {
return data; return data;
} }
function processWidgetException(exception) {
var parentScope = $window.parent.angular.element($window.frameElement).scope();
var data = parseException(exception, -5);
if ($rootScope.widgetEditMode) {
parentScope.$emit('widgetException', data);
parentScope.$apply();
}
return data;
}
function getDefaultDatasource(dataKeySchema) { function getDefaultDatasource(dataKeySchema) {
var datasource = angular.copy(defaultDatasource); var datasource = angular.copy(defaultDatasource);
if (angular.isDefined(dataKeySchema)) { if (angular.isDefined(dataKeySchema)) {

2
ui/src/app/component/component-dialog.tpl.html

@ -27,7 +27,7 @@
</md-button> </md-button>
</div> </div>
</md-toolbar> </md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear> <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span> <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content> <md-dialog-content>
<div class="md-dialog-content tb-filter"> <div class="md-dialog-content tb-filter">

27
ui/src/app/components/dashboard-select.directive.js

@ -40,27 +40,19 @@ function DashboardSelect($compile, $templateCache, $q, dashboardService, userSer
scope.dashboard = null; scope.dashboard = null;
scope.dashboardSearchText = ''; scope.dashboardSearchText = '';
scope.dashboardFetchFunction = dashboardService.getTenantDashboards;
if (angular.isDefined(scope.dashboardsScope)) {
if (scope.dashboardsScope === 'customer') {
scope.dashboardFetchFunction = dashboardService.getCustomerDashboards;
} else {
scope.dashboardFetchFunction = dashboardService.getTenantDashboards;
}
} else {
if (userService.getAuthority() === 'TENANT_ADMIN') {
scope.dashboardFetchFunction = dashboardService.getTenantDashboards;
} else if (userService.getAuthority() === 'CUSTOMER_USER') {
scope.dashboardFetchFunction = dashboardService.getCustomerDashboards;
}
}
scope.fetchDashboards = function(searchText) { scope.fetchDashboards = function(searchText) {
var pageLink = {limit: 10, textSearch: searchText}; var pageLink = {limit: 10, textSearch: searchText};
var deferred = $q.defer(); var deferred = $q.defer();
scope.dashboardFetchFunction(pageLink).then(function success(result) { var promise;
if (scope.dashboardsScope === 'customer' || userService.getAuthority() === 'CUSTOMER_USER') {
promise = dashboardService.getCustomerDashboards(scope.customerId, pageLink);
} else {
promise = dashboardService.getTenantDashboards(pageLink);
}
promise.then(function success(result) {
deferred.resolve(result.data); deferred.resolve(result.data);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -79,6 +71,8 @@ function DashboardSelect($compile, $templateCache, $q, dashboardService, userSer
ngModelCtrl.$render = function () { ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
scope.dashboard = ngModelCtrl.$viewValue; scope.dashboard = ngModelCtrl.$viewValue;
} else {
scope.dashboard = null;
} }
} }
@ -106,6 +100,7 @@ function DashboardSelect($compile, $templateCache, $q, dashboardService, userSer
link: linker, link: linker,
scope: { scope: {
dashboardsScope: '@', dashboardsScope: '@',
customerId: '=',
theForm: '=?', theForm: '=?',
tbRequired: '=?', tbRequired: '=?',
selectFirstDashboard: '=' selectFirstDashboard: '='

247
ui/src/app/components/dashboard.directive.js

@ -51,7 +51,8 @@ function Dashboard() {
scope: true, scope: true,
bindToController: { bindToController: {
widgets: '=', widgets: '=',
deviceAliasList: '=', aliasesInfo: '=',
dashboardTimewindow: '=?',
columns: '=', columns: '=',
margins: '=', margins: '=',
isEdit: '=', isEdit: '=',
@ -68,9 +69,11 @@ function Dashboard() {
prepareDashboardContextMenu: '&?', prepareDashboardContextMenu: '&?',
prepareWidgetContextMenu: '&?', prepareWidgetContextMenu: '&?',
loadWidgets: '&?', loadWidgets: '&?',
getStDiff: '&?',
onInit: '&?', onInit: '&?',
onInitFailed: '&?', onInitFailed: '&?',
dashboardStyle: '=?' dashboardStyle: '=?',
dashboardClass: '=?'
}, },
controller: DashboardController, controller: DashboardController,
controllerAs: 'vm', controllerAs: 'vm',
@ -79,7 +82,7 @@ function Dashboard() {
} }
/*@ngInject*/ /*@ngInject*/
function DashboardController($scope, $rootScope, $element, $timeout, $log, toast, types) { function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types) {
var highlightedMode = false; var highlightedMode = false;
var highlightedWidget = null; var highlightedWidget = null;
@ -87,16 +90,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
var mouseDownWidget = -1; var mouseDownWidget = -1;
var widgetMouseMoved = false; var widgetMouseMoved = false;
var gridsterParent = null; var gridsterParent = $('#gridster-parent', $element);
var gridsterElement = null; var gridsterElement = angular.element($('#gridster-child', gridsterParent));
var vm = this; var vm = this;
vm.gridster = null; vm.gridster = null;
vm.stDiff = 0;
vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false; vm.isMobileDisabled = angular.isDefined(vm.isMobileDisabled) ? vm.isMobileDisabled : false;
if (!('dashboardTimewindow' in vm)) {
vm.dashboardTimewindow = timeService.defaultTimewindow();
}
vm.dashboardLoading = true; vm.dashboardLoading = true;
vm.visibleRect = { vm.visibleRect = {
top: 0, top: 0,
@ -105,9 +113,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
right: 0 right: 0
}; };
vm.gridsterOpts = { vm.gridsterOpts = {
pushing: false,
floating: false, floating: false,
swapping: false,
maxRows: 100, maxRows: 100,
columns: vm.columns ? vm.columns : 24, columns: vm.columns ? vm.columns : 24,
margins: vm.margins ? vm.margins : [10, 10],
minSizeX: 2, minSizeX: 2,
minSizeY: 2, minSizeY: 2,
defaultSizeX: 8, defaultSizeX: 8,
@ -118,22 +129,13 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
draggable: { draggable: {
enabled: vm.isEdit enabled: vm.isEdit
}, },
isMobile: vm.isMobileDisabled ? false : vm.isMobile,
mobileBreakPoint: vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960),
margins: vm.margins ? vm.margins : [10, 10],
saveGridItemCalculatedHeightInMobile: true saveGridItemCalculatedHeightInMobile: true
}; };
updateMobileOpts();
vm.widgetItemMap = { vm.widgetItemMap = {
sizeX: 'widget.sizeX', sizeX: 'vm.widgetSizeX(widget)',
sizeY: 'widget.sizeY',
row: 'widget.row',
col: 'widget.col',
minSizeY: 'widget.minSizeY',
maxSizeY: 'widget.maxSizeY'
};
vm.mobileWidgetItemMap = {
sizeX: 'widget.sizeX',
sizeY: 'vm.widgetSizeY(widget)', sizeY: 'vm.widgetSizeY(widget)',
row: 'widget.row', row: 'widget.row',
col: 'widget.col', col: 'widget.col',
@ -141,8 +143,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
maxSizeY: 'widget.maxSizeY' maxSizeY: 'widget.maxSizeY'
}; };
vm.currentWidgetItemMap = vm.gridsterOpts.isMobile ? vm.mobileWidgetItemMap : vm.widgetItemMap;
vm.isWidgetExpanded = false; vm.isWidgetExpanded = false;
vm.isHighlighted = isHighlighted; vm.isHighlighted = isHighlighted;
vm.isNotHighlighted = isNotHighlighted; vm.isNotHighlighted = isNotHighlighted;
@ -156,6 +156,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.widgetMouseMove = widgetMouseMove; vm.widgetMouseMove = widgetMouseMove;
vm.widgetMouseUp = widgetMouseUp; vm.widgetMouseUp = widgetMouseUp;
vm.widgetSizeX = widgetSizeX;
vm.widgetSizeY = widgetSizeY; vm.widgetSizeY = widgetSizeY;
vm.widgetColor = widgetColor; vm.widgetColor = widgetColor;
vm.widgetBackgroundColor = widgetBackgroundColor; vm.widgetBackgroundColor = widgetBackgroundColor;
@ -181,13 +182,25 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.widgetContextMenuItems = []; vm.widgetContextMenuItems = [];
vm.widgetContextMenuEvent = null; vm.widgetContextMenuEvent = null;
vm.dashboardTimewindowApi = {
onResetTimewindow: function() {
if (vm.originalDashboardTimewindow) {
vm.dashboardTimewindow = angular.copy(vm.originalDashboardTimewindow);
vm.originalDashboardTimewindow = null;
}
},
onUpdateTimewindow: function(startTimeMs, endTimeMs) {
if (!vm.originalDashboardTimewindow) {
vm.originalDashboardTimewindow = angular.copy(vm.dashboardTimewindow);
}
vm.dashboardTimewindow = timeService.toHistoryTimewindow(vm.dashboardTimewindow, startTimeMs, endTimeMs);
}
};
//$element[0].onmousemove=function(){ //$element[0].onmousemove=function(){
// widgetMouseMove(); // widgetMouseMove();
// } // }
gridsterParent = $('#gridster-parent', $element);
gridsterElement = angular.element($('#gridster-child', gridsterParent));
//TODO: widgets visibility //TODO: widgets visibility
/*gridsterParent.scroll(function () { /*gridsterParent.scroll(function () {
updateVisibleRect(); updateVisibleRect();
@ -197,34 +210,62 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
updateVisibleRect(); updateVisibleRect();
});*/ });*/
function updateMobileOpts() {
var isMobileDisabled = vm.isMobileDisabled === true;
var isMobile = vm.isMobile === true && !isMobileDisabled;
var mobileBreakPoint = isMobileDisabled ? 0 : (isMobile ? 20000 : 960);
if (!isMobile && !isMobileDisabled) {
isMobile = !$mdMedia('gt-sm');
}
var rowHeight = isMobile ? 70 : 'match';
if (vm.gridsterOpts.isMobile != isMobile) {
vm.gridsterOpts.isMobile = isMobile;
vm.gridsterOpts.mobileModeEnabled = isMobile;
}
if (vm.gridsterOpts.mobileBreakPoint != mobileBreakPoint) {
vm.gridsterOpts.mobileBreakPoint = mobileBreakPoint;
}
if (vm.gridsterOpts.rowHeight != rowHeight) {
vm.gridsterOpts.rowHeight = rowHeight;
}
}
$scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {
updateMobileOpts();
});
$scope.$watch('vm.isMobile', function () { $scope.$watch('vm.isMobile', function () {
vm.gridsterOpts.isMobile = vm.isMobileDisabled ? false : vm.isMobile; updateMobileOpts();
vm.gridsterOpts.mobileBreakPoint = vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960);
}); });
$scope.$watch('vm.isMobileDisabled', function () { $scope.$watch('vm.isMobileDisabled', function () {
vm.gridsterOpts.isMobile = vm.isMobileDisabled ? false : vm.isMobile; updateMobileOpts();
vm.gridsterOpts.mobileBreakPoint = vm.isMobileDisabled ? 0 : (vm.isMobile ? 20000 : 960);
}); });
$scope.$watch('vm.columns', function () { $scope.$watch('vm.columns', function () {
vm.gridsterOpts.columns = vm.columns ? vm.columns : 24; var columns = vm.columns ? vm.columns : 24;
if (vm.gridster) { if (vm.gridsterOpts.columns != columns) {
vm.gridster.columns = vm.columns; vm.gridsterOpts.columns = columns;
updateGridsterParams(); if (vm.gridster) {
vm.gridster.columns = vm.columns;
updateGridsterParams();
}
//TODO: widgets visibility
//updateVisibleRect();
} }
//TODO: widgets visibility
//updateVisibleRect();
}); });
$scope.$watch('vm.margins', function () { $scope.$watch('vm.margins', function () {
vm.gridsterOpts.margins = vm.margins ? vm.margins : [10, 10]; var margins = vm.margins ? vm.margins : [10, 10];
if (vm.gridster) { if (!angular.equals(vm.gridsterOpts.margins, margins)) {
vm.gridster.margins = vm.margins; vm.gridsterOpts.margins = margins;
updateGridsterParams(); if (vm.gridster) {
vm.gridster.margins = vm.margins;
updateGridsterParams();
}
//TODO: widgets visibility
//updateVisibleRect();
} }
//TODO: widgets visibility
//updateVisibleRect();
}); });
$scope.$watch('vm.isEdit', function () { $scope.$watch('vm.isEdit', function () {
@ -233,8 +274,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
$scope.$broadcast('toggleDashboardEditMode', vm.isEdit); $scope.$broadcast('toggleDashboardEditMode', vm.isEdit);
}); });
$scope.$watch('vm.deviceAliasList', function () { $scope.$watch('vm.aliasesInfo.deviceAliases', function () {
$scope.$broadcast('deviceAliasListChanged', vm.deviceAliasList); $scope.$broadcast('deviceAliasListChanged', vm.aliasesInfo);
}, true); }, true);
$scope.$on('gridster-resized', function (event, sizes, theGridster) { $scope.$on('gridster-resized', function (event, sizes, theGridster) {
@ -248,12 +289,14 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
$scope.$on('gridster-mobile-changed', function (event, theGridster) { $scope.$on('gridster-mobile-changed', function (event, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) { if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster; vm.gridster = theGridster;
if (vm.gridster.isMobile) { var rowHeight = vm.gridster.isMobile ? 70 : 'match';
vm.gridsterOpts.rowHeight = 70; if (vm.gridsterOpts.rowHeight != rowHeight) {
} else { vm.gridsterOpts.rowHeight = rowHeight;
vm.gridsterOpts.rowHeight = 'match'; updateGridsterParams();
} }
vm.currentWidgetItemMap = vm.gridster.isMobile ? vm.mobileWidgetItemMap : vm.widgetItemMap;
$scope.$broadcast('mobileModeChanged', vm.gridster.isMobile);
//TODO: widgets visibility //TODO: widgets visibility
/*$timeout(function () { /*$timeout(function () {
updateVisibleRect(true); updateVisibleRect(true);
@ -283,7 +326,28 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}); });
}); });
loadDashboard(); loadStDiff();
function loadStDiff() {
if (vm.getStDiff) {
var promise = vm.getStDiff();
if (promise) {
promise.then(function (stDiff) {
vm.stDiff = stDiff;
loadDashboard();
}, function () {
vm.stDiff = 0;
loadDashboard();
});
} else {
vm.stDiff = 0;
loadDashboard();
}
} else {
vm.stDiff = 0;
loadDashboard();
}
}
function loadDashboard() { function loadDashboard() {
resetWidgetClick(); resetWidgetClick();
@ -367,7 +431,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}*/ }*/
function checkIsLocalGridsterElement (gridster) { function checkIsLocalGridsterElement (gridster) {
return gridsterElement[0] === gridster.$element[0]; return gridsterElement && gridsterElement[0] === gridster.$element[0];
} }
function resetWidgetClick () { function resetWidgetClick () {
@ -440,6 +504,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
row: 0, row: 0,
column: 0 column: 0
} }
if (!gridsterParent) {
return pos;
}
var offset = gridsterParent.offset(); var offset = gridsterParent.offset();
var x = event.pageX - offset.left + gridsterParent.scrollLeft(); var x = event.pageX - offset.left + gridsterParent.scrollLeft();
var y = event.pageY - offset.top + gridsterParent.scrollTop(); var y = event.pageY - offset.top + gridsterParent.scrollTop();
@ -483,35 +550,39 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
function highlightWidget(widget, delay) { function highlightWidget(widget, delay) {
highlightedMode = true; highlightedMode = true;
highlightedWidget = widget; highlightedWidget = widget;
var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; if (vm.gridster) {
if (item) { var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)];
var height = $(item).outerHeight(true); if (item) {
var rectHeight = gridsterParent.height(); var height = $(item).outerHeight(true);
var offset = (rectHeight - height) / 2; var rectHeight = gridsterParent.height();
var scrollTop = item.offsetTop; var offset = (rectHeight - height) / 2;
if (offset > 0) { var scrollTop = item.offsetTop;
scrollTop -= offset; if (offset > 0) {
scrollTop -= offset;
}
gridsterParent.animate({
scrollTop: scrollTop
}, delay);
} }
gridsterParent.animate({
scrollTop: scrollTop
}, delay);
} }
} }
function selectWidget(widget, delay) { function selectWidget(widget, delay) {
selectedWidget = widget; selectedWidget = widget;
var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)]; if (vm.gridster) {
if (item) { var item = $('.gridster-item', vm.gridster.$element)[vm.widgets.indexOf(widget)];
var height = $(item).outerHeight(true); if (item) {
var rectHeight = gridsterParent.height(); var height = $(item).outerHeight(true);
var offset = (rectHeight - height) / 2; var rectHeight = gridsterParent.height();
var scrollTop = item.offsetTop; var offset = (rectHeight - height) / 2;
if (offset > 0) { var scrollTop = item.offsetTop;
scrollTop -= offset; if (offset > 0) {
scrollTop -= offset;
}
gridsterParent.animate({
scrollTop: scrollTop
}, delay);
} }
gridsterParent.animate({
scrollTop: scrollTop
}, delay);
} }
} }
@ -533,9 +604,17 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
return highlightedMode && highlightedWidget != widget; return highlightedMode && highlightedWidget != widget;
} }
function widgetSizeX(widget) {
return widget.sizeX;
}
function widgetSizeY(widget) { function widgetSizeY(widget) {
if (vm.gridster && vm.gridster.isMobile && widget.config.mobileHeight) { if (vm.gridsterOpts.isMobile) {
return widget.config.mobileHeight; if (widget.config.mobileHeight) {
return widget.config.mobileHeight;
} else {
return widget.sizeY * 24 / vm.gridsterOpts.columns;
}
} else { } else {
return widget.sizeY; return widget.sizeY;
} }
@ -598,7 +677,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
} }
function hasTimewindow(widget) { function hasTimewindow(widget) {
return widget.type === types.widgetType.timeseries.value; if (widget.type === types.widgetType.timeseries.value) {
return angular.isDefined(widget.config.useDashboardTimewindow) ?
!widget.config.useDashboardTimewindow : false;
} else {
return false;
}
} }
function adoptMaxRows() { function adoptMaxRows() {
@ -614,11 +698,20 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
} }
function dashboardLoaded() { function dashboardLoaded() {
adoptMaxRows(); $timeout(function () {
vm.dashboardLoading = false; $scope.$watch('vm.dashboardTimewindow', function () {
if (vm.onInit) { $scope.$broadcast('dashboardTimewindowChanged', vm.dashboardTimewindow);
vm.onInit({dashboard: vm}); }, true);
} adoptMaxRows();
vm.dashboardLoading = false;
$timeout(function () {
var gridsterScope = gridsterElement.scope();
vm.gridster = gridsterScope.gridster;
if (vm.onInit) {
vm.onInit({dashboard: vm});
}
}, 0, false);
}, 0, false);
} }
function loading() { function loading() {

6
ui/src/app/components/dashboard.scss

@ -51,7 +51,7 @@ div.tb-widget {
height: 32px; height: 32px;
min-width: 32px; min-width: 32px;
min-height: 32px; min-height: 32px;
md-icon { md-icon, ng-md-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
min-width: 20px; min-width: 20px;
@ -93,6 +93,10 @@ md-content.tb-dashboard-content {
right: 0; right: 0;
bottom: 0; bottom: 0;
outline: none; outline: none;
background: none;
.gridster-item {
@include transition(none);
}
} }
.tb-widget-error-container { .tb-widget-error-container {

39
ui/src/app/components/dashboard.tpl.html

@ -17,19 +17,20 @@
--> -->
<md-content flex layout="column" class="tb-progress-cover" layout-align="center center" <md-content flex layout="column" class="tb-progress-cover" layout-align="center center"
ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit"> ng-show="(vm.loading() || vm.dashboardLoading) && !vm.isEdit">
<md-progress-circular md-mode="indeterminate" class="md-warn" md-diameter="100"></md-progress-circular> <md-progress-circular md-mode="indeterminate" ng-disabled="!vm.loading() && !vm.dashboardLoading || vm.isEdit" class="md-warn" md-diameter="100"></md-progress-circular>
</md-content> </md-content>
<md-menu md-position-mode="target target" tb-mousepoint-menu> <md-menu md-position-mode="target target" tb-mousepoint-menu>
<md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)"> <md-content id="gridster-parent" class="tb-dashboard-content" flex layout-wrap ng-click="" tb-contextmenu="vm.openDashboardContextMenu($event, $mdOpenMousepointMenu)">
<div ng-style="vm.dashboardStyle" id="gridster-background" style="height: auto; min-height: 100%;"> <div ng-class="vm.dashboardClass" id="gridster-background" style="height: auto; min-height: 100%;">
<div id="gridster-child" gridster="vm.gridsterOpts"> <div id="gridster-child" gridster="vm.gridsterOpts">
<ul> <ul>
<!-- ng-click="widgetClicked($event, widget)" --> <!-- ng-click="widgetClicked($event, widget)" -->
<li gridster-item="vm.currentWidgetItemMap" ng-repeat="widget in vm.widgets"> <li gridster-item="vm.widgetItemMap" class="tb-noselect" ng-repeat="widget in vm.widgets">
<md-menu md-position-mode="target target" tb-mousepoint-menu> <md-menu md-position-mode="target target" tb-mousepoint-menu>
<div tb-expand-fullscreen <div tb-expand-fullscreen
fullscreen-background-style="vm.dashboardStyle" fullscreen-background-style="vm.dashboardStyle"
expand-button-id="expand-button" expand-button-id="expand-button"
expand-button-size="20"
on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)"
layout="column" layout="column"
class="tb-widget" class="tb-widget"
@ -45,55 +46,55 @@
color: vm.widgetColor(widget), color: vm.widgetColor(widget),
backgroundColor: vm.widgetBackgroundColor(widget), backgroundColor: vm.widgetBackgroundColor(widget),
padding: vm.widgetPadding(widget)}"> padding: vm.widgetPadding(widget)}">
<div class="tb-widget-title" layout="column" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)"> <div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitle(widget) || vm.hasTimewindow(widget)">
<span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span> <span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{widget.config.title}}</span>
<tb-timewindow ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> <tb-timewindow aggregation ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div> </div>
<div class="tb-widget-actions" layout="row" layout-align="start center"> <div class="tb-widget-actions" layout="row" layout-align="start center">
<md-button id="expand-button" <md-button id="expand-button"
ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)" ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
aria-label="{{ 'fullscreen.fullscreen' | translate }}" aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button md-primary"></md-button> class="md-icon-button"></md-button>
<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded" <md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()" ng-disabled="vm.loading()"
class="md-icon-button md-primary" class="md-icon-button"
ng-click="vm.editWidget($event, widget)" ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}"> aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
{{ 'widget.edit' | translate }} {{ 'widget.edit' | translate }}
</md-tooltip> </md-tooltip>
<md-icon class="material-icons"> <ng-md-icon size="20" icon="edit"></ng-md-icon>
edit
</md-icon>
</md-button> </md-button>
<md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded" <md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()" ng-disabled="vm.loading()"
class="md-icon-button md-primary" class="md-icon-button"
ng-click="vm.exportWidget($event, widget)" ng-click="vm.exportWidget($event, widget)"
aria-label="{{ 'widget.export' | translate }}"> aria-label="{{ 'widget.export' | translate }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
{{ 'widget.export' | translate }} {{ 'widget.export' | translate }}
</md-tooltip> </md-tooltip>
<md-icon class="material-icons"> <ng-md-icon size="20" icon="file_download"></ng-md-icon>
file_download
</md-icon>
</md-button> </md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded" <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()" ng-disabled="vm.loading()"
class="md-icon-button md-primary" class="md-icon-button"
ng-click="vm.removeWidget($event, widget)" ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}"> aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
{{ 'widget.remove' | translate }} {{ 'widget.remove' | translate }}
</md-tooltip> </md-tooltip>
<md-icon class="material-icons"> <ng-md-icon size="20" icon="close"></ng-md-icon>
close
</md-icon>
</md-button> </md-button>
</div> </div>
<div flex layout="column" class="tb-widget-content"> <div flex layout="column" class="tb-widget-content">
<div flex tb-widget <div flex tb-widget
locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }"> locals="{ visibleRect: vm.visibleRect,
widget: widget,
aliasesInfo: vm.aliasesInfo,
isEdit: vm.isEdit,
stDiff: vm.stDiff,
dashboardTimewindow: vm.dashboardTimewindow,
dashboardTimewindowApi: vm.dashboardTimewindowApi }">
</div> </div>
</div> </div>
</div> </div>

2
ui/src/app/components/datakey-config-dialog.tpl.html

@ -26,7 +26,7 @@
</md-button> </md-button>
</div> </div>
</md-toolbar> </md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear> <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span> <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content> <md-dialog-content>
<tb-datakey-config ng-model="vm.dataKey" <tb-datakey-config ng-model="vm.dataKey"

217
ui/src/app/components/device-filter.directive.js

@ -0,0 +1,217 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import deviceFilterTemplate from './device-filter.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import './device-filter.scss';
export default angular.module('thingsboard.directives.deviceFilter', [])
.directive('tbDeviceFilter', DeviceFilter)
.name;
/*@ngInject*/
function DeviceFilter($compile, $templateCache, $q, deviceService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(deviceFilterTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.fetchDevices = function(searchText, limit) {
var pageLink = {limit: limit, textSearch: searchText};
var deferred = $q.defer();
deviceService.getTenantDevices(pageLink).then(function success(result) {
deferred.resolve(result.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
scope.updateValidity = function() {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var valid;
if (value.useFilter) {
ngModelCtrl.$setValidity('deviceList', true);
if (angular.isDefined(value.deviceNameFilter) && value.deviceNameFilter.length > 0) {
ngModelCtrl.$setValidity('deviceNameFilter', true);
valid = angular.isDefined(scope.model.matchingFilterDevice) && scope.model.matchingFilterDevice != null;
ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', valid);
} else {
ngModelCtrl.$setValidity('deviceNameFilter', false);
}
} else {
ngModelCtrl.$setValidity('deviceNameFilter', true);
ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', true);
valid = angular.isDefined(value.deviceList) && value.deviceList.length > 0;
ngModelCtrl.$setValidity('deviceList', valid);
}
}
}
ngModelCtrl.$render = function () {
destroyWatchers();
scope.model = {
useFilter: false,
deviceList: [],
deviceNameFilter: ''
}
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var model = scope.model;
model.useFilter = value.useFilter === true ? true: false;
model.deviceList = [];
model.deviceNameFilter = value.deviceNameFilter || '';
processDeviceNameFilter(model.deviceNameFilter).then(
function(device) {
scope.model.matchingFilterDevice = device;
if (value.deviceList && value.deviceList.length > 0) {
deviceService.getDevices(value.deviceList).then(function (devices) {
model.deviceList = devices;
updateMatchingDevice();
initWatchers();
});
} else {
updateMatchingDevice();
initWatchers();
}
}
)
}
}
function updateMatchingDevice() {
if (scope.model.useFilter) {
scope.model.matchingDevice = scope.model.matchingFilterDevice;
} else {
if (scope.model.deviceList && scope.model.deviceList.length > 0) {
scope.model.matchingDevice = scope.model.deviceList[0];
} else {
scope.model.matchingDevice = null;
}
}
}
function processDeviceNameFilter(deviceNameFilter) {
var deferred = $q.defer();
if (angular.isDefined(deviceNameFilter) && deviceNameFilter.length > 0) {
scope.fetchDevices(deviceNameFilter, 1).then(function (devices) {
if (devices && devices.length > 0) {
deferred.resolve(devices[0]);
} else {
deferred.resolve(null);
}
});
} else {
deferred.resolve(null);
}
return deferred.promise;
}
function destroyWatchers() {
if (scope.deviceListDeregistration) {
scope.deviceListDeregistration();
scope.deviceListDeregistration = null;
}
if (scope.useFilterDeregistration) {
scope.useFilterDeregistration();
scope.useFilterDeregistration = null;
}
if (scope.deviceNameFilterDeregistration) {
scope.deviceNameFilterDeregistration();
scope.deviceNameFilterDeregistration = null;
}
if (scope.matchingDeviceDeregistration) {
scope.matchingDeviceDeregistration();
scope.matchingDeviceDeregistration = null;
}
}
function initWatchers() {
scope.deviceListDeregistration = scope.$watch('model.deviceList', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.deviceList = [];
if (scope.model.deviceList && scope.model.deviceList.length > 0) {
for (var i in scope.model.deviceList) {
value.deviceList.push(scope.model.deviceList[i].id.id);
}
}
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
}, true);
scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.useFilter = scope.model.useFilter;
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
});
scope.deviceNameFilterDeregistration = scope.$watch('model.deviceNameFilter', function (newNameFilter, prevNameFilter) {
if (ngModelCtrl.$viewValue) {
if (!angular.equals(newNameFilter, prevNameFilter)) {
var value = ngModelCtrl.$viewValue;
value.deviceNameFilter = scope.model.deviceNameFilter;
processDeviceNameFilter(value.deviceNameFilter).then(
function(device) {
scope.model.matchingFilterDevice = device;
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
);
}
}
});
scope.matchingDeviceDeregistration = scope.$watch('model.matchingDevice', function (newMatchingDevice, prevMatchingDevice) {
if (!angular.equals(newMatchingDevice, prevMatchingDevice)) {
if (scope.onMatchingDeviceChange) {
scope.onMatchingDeviceChange({device: newMatchingDevice});
}
}
});
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
isEdit: '=',
onMatchingDeviceChange: '&'
}
};
}

45
ui/src/app/components/device-filter.scss

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-device-filter {
#device_list_chips {
.md-chips {
padding-bottom: 1px;
}
}
.device-name-filter-input {
margin-top: 10px;
margin-bottom: 0px;
.md-errors-spacer {
min-height: 0px;
}
}
.tb-filter-switch {
padding-left: 10px;
.filter-switch {
margin: 0;
}
.filter-label {
margin: 5px 0;
}
}
.tb-error-messages {
margin-top: -11px;
height: 35px;
.tb-error-message {
padding-left: 1px;
}
}
}

67
ui/src/app/components/device-filter.tpl.html

@ -0,0 +1,67 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section layout='column' class="tb-device-filter">
<section layout='row'>
<section layout="column" flex ng-show="!model.useFilter">
<md-chips flex
id="device_list_chips"
ng-required="!useFilter"
ng-model="model.deviceList" md-autocomplete-snap
md-require-match="true">
<md-autocomplete
md-no-cache="true"
id="device"
md-selected-item="selectedDevice"
md-search-text="deviceSearchText"
md-items="item in fetchDevices(deviceSearchText, 10)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'device.device-list' | translate }}">
<md-item-template>
<span md-highlight-text="deviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ device: deviceSearchText }'>device.no-devices-matching</span>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<span>
<strong>{{$chip.name}}</strong>
</span>
</md-chip-template>
</md-chips>
</section>
<section layout="row" flex ng-show="model.useFilter">
<md-input-container flex class="device-name-filter-input">
<label translate>device.name-starts-with</label>
<input ng-model="model.deviceNameFilter" aria-label="{{ 'device.name-starts-with' | translate }}">
</md-input-container>
</section>
<section class="tb-filter-switch" layout="column" layout-align="center center">
<label class="tb-small filter-label" translate>device.use-device-name-filter</label>
<md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
</md-switch>
</section>
</section>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="deviceList" class="tb-error-message">device.device-list-empty</div>
<div translate ng-message="deviceNameFilter" class="tb-error-message">device.device-name-filter-required</div>
<div translate translate-values='{ device: model.deviceNameFilter }' ng-message="deviceNameFilterDeviceMatch"
class="tb-error-message">device.device-name-filter-no-device-matched</div>
</div>
</section>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save