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>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<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.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Predicate;
@ -85,14 +86,22 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
this.attributeSubscriptions = new HashMap<>();
this.rpcSubscriptions = new HashMap<>();
this.rpcPendingMap = new HashMap<>();
refreshAttributes();
initAttributes();
}
private void refreshAttributes() {
private void initAttributes() {
this.deviceAttributes = new DeviceAttributes(fetchAttributes(DataConstants.CLIENT_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) {
ToDeviceRpcRequest request = msg.getMsg();
ToDeviceRpcRequestBody body = request.getBody();
@ -195,10 +204,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
//TODO: improve this procedure to fetch only changed attributes.
refreshAttributes();
//TODO: support attributes deletion
Set<AttributeKey> keys = msg.getKeys();
refreshAttributes(msg);
Set<AttributeKey> keys = msg.getDeletedKeys();
if (attributeSubscriptions.size() > 0) {
ToDeviceMsg notification = null;
if (msg.isDeleted()) {
@ -360,8 +367,14 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
}
}
private List<AttributeKvEntry> fetchAttributes(String attributeType) {
return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
private List<AttributeKvEntry> fetchAttributes(String scope) {
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) {

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

@ -173,7 +173,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
@Override
public void onUpdate(ActorContext context) throws Exception {
PluginMetaData oldPluginMd = systemContext.getPluginService().findPluginById(entityId);
PluginMetaData oldPluginMd = pluginMd;
pluginMd = systemContext.getPluginService().findPluginById(entityId);
boolean requiresRestart = false;
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.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
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 akka.actor.ActorRef;
import org.w3c.dom.Attr;
import javax.annotation.Nullable;
@ -88,109 +90,120 @@ public final class PluginProcessingContext implements PluginContext {
}
@Override
public void saveAttributes(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes, PluginCallback<Void> callback) {
validate(deviceId);
Set<AttributeKey> keys = new HashSet<>();
for (AttributeKvEntry attribute : attributes) {
keys.add(new AttributeKey(scope, attribute.getKey()));
}
public void saveAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List<AttributeKvEntry> attributes, final PluginCallback<Void> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> {
onDeviceAttributesChanged(tenantId, deviceId, scope, attributes);
return null;
}), executor);
}));
}
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> {
onDeviceAttributesChanged(deviceId, keys);
return null;
}), executor);
@Override
public void removeAttributes(final TenantId tenantId, final DeviceId deviceId, final String scope, final List<String> keys, final PluginCallback<Void> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> future = pluginCtx.attributesService.removeAll(deviceId, scope, keys);
Futures.addCallback(future, getCallback(callback, v -> null), executor);
onDeviceAttributesDeleted(tenantId, deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet()));
}));
}
@Override
public Optional<AttributeKvEntry> loadAttribute(DeviceId deviceId, String attributeType, String attributeKey) {
validate(deviceId);
AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
return Optional.ofNullable(attribute);
public void loadAttribute(DeviceId deviceId, String attributeType, String attributeKey, final PluginCallback<Optional<AttributeKvEntry>> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<Optional<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType, List<String> attributeKeys) {
validate(deviceId);
List<AttributeKvEntry> result = new ArrayList<>(attributeKeys.size());
for (String attributeKey : attributeKeys) {
AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
if (attribute != null) {
result.add(attribute);
}
}
return result;
public void loadAttributes(DeviceId deviceId, String attributeType, Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType) {
validate(deviceId);
return pluginCtx.attributesService.findAll(deviceId, attributeType);
public void loadAttributes(DeviceId deviceId, String attributeType, PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<AttributeKvEntry>> future = pluginCtx.attributesService.findAll(deviceId, attributeType);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
public void removeAttributes(DeviceId deviceId, String scope, List<String> keys) {
validate(deviceId);
pluginCtx.attributesService.removeAll(deviceId, scope, keys);
onDeviceAttributesDeleted(deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet()));
public void loadAttributes(final DeviceId deviceId, final Collection<String> attributeTypes, final PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.findAll(deviceId, attributeType)));
convertFuturesAndAddCallback(callback, futures);
}));
}
@Override
public void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback) {
validate(deviceId);
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
public void loadAttributes(final DeviceId deviceId, final Collection<String> attributeTypes, final Collection<String> attributeKeys, final PluginCallback<List<AttributeKvEntry>> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
attributeTypes.forEach(attributeType -> futures.add(pluginCtx.attributesService.find(deviceId, attributeType, attributeKeys)));
convertFuturesAndAddCallback(callback, futures);
}));
}
@Override
public void saveTsData(DeviceId deviceId, List<TsKvEntry> entries, PluginCallback<Void> callback) {
validate(deviceId);
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
public void saveTsData(final DeviceId deviceId, final TsKvEntry entry, final PluginCallback<Void> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
}));
}
@Override
public List<TsKvEntry> loadTimeseries(DeviceId deviceId, TsKvQuery query) {
validate(deviceId);
return pluginCtx.tsService.find(DataConstants.DEVICE, deviceId, query);
public void saveTsData(final DeviceId deviceId, final List<TsKvEntry> entries, final PluginCallback<Void> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries);
Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
}));
}
@Override
public void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback) {
validate(deviceId);
ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId);
Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor);
public void loadTimeseries(final DeviceId deviceId, final List<TsKvQuery> queries, final PluginCallback<List<TsKvEntry>> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ListenableFuture<List<TsKvEntry>> future = pluginCtx.tsService.findAll(DataConstants.DEVICE, deviceId, queries);
Futures.addCallback(future, getCallback(callback, v -> v), executor);
}));
}
@Override
public void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback) {
validate(deviceId);
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);
public void loadLatestTimeseries(final DeviceId deviceId, final PluginCallback<List<TsKvEntry>> callback) {
validate(deviceId, new ValidationCallback(callback, ctx -> {
ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId);
Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor);
}));
}
@Override
public void reply(PluginToRuleMsg<?> msg) {
pluginCtx.parentActor.tell(msg, ActorRef.noSender());
public void loadLatestTimeseries(final DeviceId deviceId, final Collection<String> keys, final PluginCallback<List<TsKvEntry>> callback) {
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
public boolean checkAccess(DeviceId deviceId) {
try {
return validate(deviceId);
} catch (IllegalStateException | IllegalArgumentException e) {
return false;
}
public void reply(PluginToRuleMsg<?> msg) {
pluginCtx.parentActor.tell(msg, ActorRef.noSender());
}
@Override
@ -203,18 +216,12 @@ public final class PluginProcessingContext implements PluginContext {
return securityCtx;
}
private void onDeviceAttributesChanged(DeviceId deviceId, AttributeKey key) {
onDeviceAttributesChanged(deviceId, Collections.singleton(key));
private void onDeviceAttributesDeleted(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) {
pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onDelete(tenantId, deviceId, keys));
}
private void onDeviceAttributesDeleted(DeviceId deviceId, Set<AttributeKey> keys) {
Device device = pluginCtx.deviceService.findDeviceById(deviceId);
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 void onDeviceAttributesChanged(TenantId tenantId, DeviceId deviceId, String scope, List<AttributeKvEntry> values) {
pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, deviceId, scope, values));
}
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) {
return new FutureCallback<ResultSet>() {
private <T, R> FutureCallback<R> getCallback(final PluginCallback<T> callback, Function<R, T> transformer) {
return new FutureCallback<R>() {
@Override
public void onSuccess(@Nullable ResultSet result) {
pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, transformer.apply(result)), ActorRef.noSender());
public void onSuccess(@Nullable R result) {
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
@ -253,26 +264,35 @@ public final class PluginProcessingContext implements PluginContext {
};
}
// TODO: replace with our own exceptions
private boolean validate(DeviceId deviceId) {
@Override
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()) {
PluginApiCallSecurityContext ctx = securityCtx.get();
final PluginApiCallSecurityContext ctx = securityCtx.get();
if (ctx.isTenantAdmin() || ctx.isCustomerUser()) {
Device device = pluginCtx.deviceService.findDeviceById(deviceId);
if (device == null) {
throw new IllegalStateException("Device not found!");
} else {
if (!device.getTenantId().equals(ctx.getTenantId())) {
throw new IllegalArgumentException("Device belongs to different tenant!");
} else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) {
throw new IllegalArgumentException("Device belongs to different customer!");
ListenableFuture<Device> deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(deviceId);
Futures.addCallback(deviceFuture, getCallback(callback, device -> {
if (device == null) {
return Boolean.FALSE;
} else {
if (!device.getTenantId().equals(ctx.getTenantId())) {
return Boolean.FALSE;
} else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) {
return Boolean.FALSE;
} else {
return Boolean.TRUE;
}
}
}
}));
} else {
return false;
callback.onSuccess(this, Boolean.FALSE);
}
} else {
callback.onSuccess(this, Boolean.TRUE);
}
return true;
}
@Override
@ -282,9 +302,8 @@ public final class PluginProcessingContext implements PluginContext {
@Override
public void getDevice(DeviceId deviceId, PluginCallback<Device> callback) {
//TODO: add caching here with async api.
Device device = pluginCtx.deviceService.findDeviceById(deviceId);
pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, device), ActorRef.noSender());
ListenableFuture<Device> deviceFuture = pluginCtx.deviceService.findDeviceByIdAsync(deviceId);
Futures.addCallback(deviceFuture, getCallback(callback, v -> v));
}
@Override
@ -303,4 +322,15 @@ public final class PluginProcessingContext implements PluginContext {
public void scheduleTimeoutMsg(TimeoutMsg 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) {
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")
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')")
@RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
@ResponseBody

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
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.DeviceId;
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.TextPageLink;
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.exception.ThingsboardException;
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
@RequestMapping("/api")
@ -189,4 +196,30 @@ public class DeviceController extends BaseController {
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
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
# 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
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 com.google.common.util.concurrent.Futures;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.common.data.id.*;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -226,7 +227,9 @@ public class DefaultActorServiceTest {
when(pluginMock.getConfiguration()).thenReturn(pluginAdditionalInfo);
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();
Thread.sleep(1000);

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<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;
import java.util.Optional;
import lombok.Data;
@Data
public class BaseTsKvQuery implements TsKvQuery {
private String key;
private Optional<Long> startTs;
private Optional<Long> endTs;
private Optional<Integer> limit;
private final String key;
private final long startTs;
private final long endTs;
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.startTs = startTs;
this.endTs = endTs;
this.interval = interval;
this.limit = limit;
}
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);
this.aggregation = aggregation;
}
public BaseTsKvQuery(String key, Integer limit) {
this(key, null, null, limit);
public BaseTsKvQuery(String key, long startTs, long endTs) {
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();
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>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/pom.xml

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

2
common/transport/pom.xml

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

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<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;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.core.utils.UUIDs;
import com.datastax.driver.mapping.Mapper;
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 org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.wrapper.EntityResultSet;
import org.thingsboard.server.dao.model.ModelConstants;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@ -59,6 +64,27 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
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) {
T object = null;
if (statement != null) {
@ -72,6 +98,27 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
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) {
return getMapper().saveQuery(dto);
}
@ -100,6 +147,14 @@ public abstract class AbstractModelDao<T extends BaseEntity<?>> extends Abstract
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) {
Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
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;
import com.datastax.driver.core.ResultSet;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.UUID;
@ -26,6 +27,8 @@ public interface Dao<T> {
T findById(UUID id);
ListenableFuture<T> findByIdAsync(UUID id);
T save(T t);
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;
}
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;
import com.datastax.driver.core.ResultSet;
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.kv.AttributeKvEntry;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.Optional;
/**
* @author Andrew Shvayka
*/
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);
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.kv.AttributeKvEntry;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* @author Andrew Shvayka
*/
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);
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.querybuilder.QueryBuilder;
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 org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.dao.AbstractDao;
import org.thingsboard.server.dao.AbstractAsyncDao;
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.dao.timeseries.BaseTimeseriesDao;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.dao.model.ModelConstants.*;
import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
@ -40,29 +45,55 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.*;
*/
@Component
@Slf4j
public class BaseAttributesDao extends AbstractDao implements AttributesDao {
public class BaseAttributesDao extends AbstractAsyncDao implements AttributesDao {
private PreparedStatement saveStmt;
@PostConstruct
public void init() {
super.startExecutor();
}
@PreDestroy
public void stop() {
super.stopExecutor();
}
@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)
.where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType()))
.and(eq(ENTITY_ID_COLUMN, entityId.getId()))
.and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType))
.and(eq(ATTRIBUTE_KEY_COLUMN, 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
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)
.where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType()))
.and(eq(ENTITY_ID_COLUMN, entityId.getId()))
.and(eq(ATTRIBUTE_TYPE_COLUMN, 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
@ -93,20 +124,19 @@ public class BaseAttributesDao extends AbstractDao implements AttributesDao {
}
@Override
public void removeAll(EntityId entityId, String attributeType, List<String> keys) {
for (String key : keys) {
delete(entityId, attributeType, key);
}
public ListenableFuture<List<ResultSet>> removeAll(EntityId entityId, String attributeType, List<String> keys) {
List<ResultSetFuture> futures = keys.stream().map(key -> delete(entityId, attributeType, key)).collect(Collectors.toList());
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)
.where(eq(ENTITY_TYPE_COLUMN, entityId.getEntityType()))
.and(eq(ENTITY_ID_COLUMN, entityId.getId()))
.and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType))
.and(eq(ATTRIBUTE_KEY_COLUMN, key));
log.debug("Remove request: {}", delete.toString());
getSession().execute(delete);
return getSession().executeAsync(delete);
}
private PreparedStatement getSaveStmt() {
@ -150,5 +180,4 @@ public class BaseAttributesDao extends AbstractDao implements AttributesDao {
}
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.thingsboard.server.dao.service.Validator;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* @author Andrew Shvayka
@ -39,14 +41,21 @@ public class BaseAttributesService implements AttributesService {
private AttributesDao attributesDao;
@Override
public AttributeKvEntry find(EntityId entityId, String scope, String attributeKey) {
public ListenableFuture<Optional<AttributeKvEntry>> find(EntityId entityId, String scope, String attributeKey) {
validate(entityId, scope);
Validator.validateString(attributeKey, "Incorrect attribute key " + attributeKey);
return attributesDao.find(entityId, scope, attributeKey);
}
@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);
return attributesDao.findAll(entityId, scope);
}
@ -56,16 +65,16 @@ public class BaseAttributesService implements AttributesService {
validate(entityId, scope);
attributes.forEach(attribute -> validate(attribute));
List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(attributes.size());
for(AttributeKvEntry attribute : attributes) {
for (AttributeKvEntry attribute : attributes) {
futures.add(attributesDao.save(entityId, scope, attribute));
}
return Futures.allAsList(futures);
}
@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);
attributesDao.removeAll(entityId, scope, keys);
return attributesDao.removeAll(entityId, scope, keys);
}
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.UUID;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.dao.Dao;
@ -46,7 +47,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
* @return the list of device objects
*/
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.
*
@ -57,6 +67,16 @@ public interface DeviceDao extends Dao<DeviceEntity> {
*/
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.
*

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

@ -16,12 +16,14 @@
package org.thingsboard.server.dao.device;
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 org.thingsboard.server.dao.model.ModelConstants.*;
import java.util.*;
import com.datastax.driver.core.querybuilder.Select;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Device;
@ -61,6 +63,16 @@ public class DeviceDaoImpl extends AbstractSearchTextDao<DeviceEntity> implement
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
public List<DeviceEntity> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink 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;
}
@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
public Optional<DeviceEntity> findDevicesByTenantIdAndName(UUID tenantId, String deviceName) {
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;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.CustomerId;
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.TextPageLink;
import java.util.List;
import java.util.Optional;
public interface DeviceService {
Device findDeviceById(DeviceId deviceId);
ListenableFuture<Device> findDeviceByIdAsync(DeviceId deviceId);
Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name);
Device saveDevice(Device device);
@ -40,9 +44,13 @@ public interface DeviceService {
TextPageData<Device> findDevicesByTenantId(TenantId tenantId, TextPageLink pageLink);
ListenableFuture<List<Device>> findDevicesByTenantIdAndIdsAsync(TenantId tenantId, List<DeviceId> deviceIds);
void deleteDevicesByTenantId(TenantId tenantId);
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);
}

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

@ -15,6 +15,9 @@
*/
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 org.apache.commons.lang3.RandomStringUtils;
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.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.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateIds;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@Service
@ -70,6 +75,14 @@ public class DeviceServiceImpl implements DeviceService {
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
public Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name) {
log.trace("Executing findDeviceByTenantIdAndName [{}][{}]", tenantId, name);
@ -132,6 +145,16 @@ public class DeviceServiceImpl implements DeviceService {
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
public void deleteDevicesByTenantId(TenantId tenantId) {
log.trace("Executing deleteDevicesByTenantId, tenantId [{}]", tenantId);
@ -150,6 +173,17 @@ public class DeviceServiceImpl implements DeviceService {
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
public void unassignCustomerDevices(TenantId tenantId, CustomerId 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 com.datastax.driver.core.utils.UUIDs;
import org.apache.commons.lang3.ArrayUtils;
import org.thingsboard.server.common.data.kv.Aggregation;
public class ModelConstants {
private ModelConstants() {
}
public static UUID NULL_UUID = UUIDs.startOf(0);
/**
* Generic constants.
*/
@ -38,7 +40,7 @@ public class ModelConstants {
public static final String ALIAS_PROPERTY = "alias";
public static final String SEARCH_TEXT_PROPERTY = "search_text";
public static final String ADDITIONAL_INFO_PROPERTY = "additional_info";
/**
* 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_LAST_NAME_PROPERTY = "last_name";
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_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";
/**
* 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_ACTIVATE_TOKEN_PROPERTY = "activate_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_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";
/**
* Cassandra admin_settings constants.
*/
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_JSON_VALUE_PROPERTY = "json_value";
public static final String ADMIN_SETTINGS_BY_KEY_COLUMN_FAMILY_NAME = "admin_settings_by_key";
/**
* Cassandra contact constants.
*/
@ -97,9 +99,9 @@ public class ModelConstants {
public static final String TENANT_TITLE_PROPERTY = TITLE_PROPERTY;
public static final String TENANT_REGION_PROPERTY = "region";
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";
/**
* 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_TITLE_PROPERTY = TITLE_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";
/**
* 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_NAME_PROPERTY = "name";
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_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";
/**
* 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_ID_PROPERTY = "credentials_id";
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_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_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_TENANT_ID_PROPERTY = TENTANT_ID_PROPERTY;
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 LONG_VALUE_COLUMN = "long_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.dao.exception.IncorrectParameterException;
import java.util.List;
import java.util.UUID;
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
* <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.querybuilder.QueryBuilder;
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 org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.kv.*;
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 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.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.select;
@ -36,53 +51,186 @@ import static com.datastax.driver.core.querybuilder.QueryBuilder.select;
*/
@Component
@Slf4j
public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao {
public class BaseTimeseriesDao extends AbstractAsyncDao implements TimeseriesDao {
@Value("${cassandra.query.max_limit_per_request}")
protected Integer maxLimitPerRequest;
//@Value("${cassandra.query.min_aggregation_step_ms}")
//TODO:
private int minAggregationStepMs = 1000;
@Value("${cassandra.query.ts_key_value_partitioning}")
private String partitioning;
private TsPartitionDate tsFormat;
private PreparedStatement partitionInsertStmt;
private PreparedStatement[] latestInsertStmts;
private PreparedStatement[] saveStmts;
private PreparedStatement[] fetchStmts;
private PreparedStatement findLatestStmt;
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
public List<TsKvEntry> find(String entityType, UUID entityId, TsKvQuery query, Optional<Long> minPartition, Optional<Long> maxPartition) {
List<Row> rows = Collections.emptyList();
Long[] parts = fetchPartitions(entityType, entityId, query.getKey(), minPartition, maxPartition);
int partsLength = parts.length;
if (parts != null && partsLength > 0) {
int limit = maxLimitPerRequest;
Optional<Integer> lim = query.getLimit();
if (lim.isPresent() && lim.get() < maxLimitPerRequest) {
limit = lim.get();
public ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, List<TsKvQuery> queries) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(entityType, entityId, query)).collect(Collectors.toList());
return Futures.transform(Futures.allAsList(futures), new Function<List<List<TsKvEntry>>, List<TsKvEntry>>() {
@Nullable
@Override
public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) {
List<TsKvEntry> result = new ArrayList<TsKvEntry>();
results.forEach(r -> result.addAll(r));
return result;
}
}, readResultsProcessingExecutor);
}
rows = new ArrayList<>(limit);
int lastIdx = partsLength - 1;
for (int i = 0; i < partsLength; i++) {
int currentLimit;
if (rows.size() >= limit) {
break;
} else {
currentLimit = limit - rows.size();
}
Long partition = parts[i];
Select.Where where = select().from(ModelConstants.TS_KV_CF).where(eq(ModelConstants.ENTITY_TYPE_COLUMN, entityType))
.and(eq(ModelConstants.ENTITY_ID_COLUMN, entityId))
.and(eq(ModelConstants.KEY_COLUMN, query.getKey()))
.and(eq(ModelConstants.PARTITION_COLUMN, partition));
if (i == 0 && query.getStartTs().isPresent()) {
where.and(QueryBuilder.gt(ModelConstants.TS_COLUMN, query.getStartTs().get()));
} else if (i == lastIdx && query.getEndTs().isPresent()) {
where.and(QueryBuilder.lte(ModelConstants.TS_COLUMN, query.getEndTs().get()));
private ListenableFuture<List<TsKvEntry>> findAllAsync(String entityType, UUID entityId, TsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(entityType, entityId, query);
} else {
long step = Math.max(query.getInterval(), minAggregationStepMs);
long stepTs = query.getStartTs();
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
while (stepTs < query.getEndTs()) {
long startTs = stepTs;
long endTs = stepTs + step;
TsKvQuery subQuery = new BaseTsKvQuery(query.getKey(), startTs, endTs, step, 1, query.getAggregation());
futures.add(findAndAggregateAsync(entityType, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
stepTs = endTs;
}
ListenableFuture<List<Optional<TsKvEntry>>> future = Futures.allAsList(futures);
return Futures.transform(future, new Function<List<Optional<TsKvEntry>>, List<TsKvEntry>>() {
@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);
rows.addAll(executeRead(where).all());
}, readResultsProcessingExecutor);
}
}
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
@ -190,13 +338,12 @@ public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao {
* Select existing partitions from the table
* <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))
.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())));
maxPartition.ifPresent(endTs -> select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition.get())));
ResultSet resultSet = executeRead(select);
return resultSet.all().stream().map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)).toArray(Long[]::new);
select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
return executeAsyncRead(select);
}
private PreparedStatement getSaveStmt(DataType dataType) {
@ -216,6 +363,30 @@ public class BaseTimeseriesDao extends AbstractDao implements TimeseriesDao {
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) {
if (latestInsertStmts == null) {
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.ResultSetFuture;
import com.datastax.driver.core.Row;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
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.TsKvQuery;
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.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.service.Validator;
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.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
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;
@Value("${cassandra.query.ts_key_value_partitioning}")
private String partitioning;
@Autowired
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
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(query);
return timeseriesDao.find(entityType, entityId.getId(), query, toPartitionTs(query.getStartTs()), toPartitionTs(query.getEndTs()));
}
private Optional<Long> toPartitionTs(Optional<Long> ts) {
if (ts.isPresent()) {
return Optional.of(toPartitionTs(ts.get()));
} else {
return Optional.empty();
}
queries.forEach(query -> validate(query));
return timeseriesDao.findAllAsync(entityType, entityId.getId(), queries);
}
@Override
@ -106,7 +87,7 @@ public class BaseTimeseriesService implements TimeseriesService {
throw new IncorrectParameterException("Key value entry can't be null");
}
UUID uid = entityId.getId();
long partitionTs = toPartitionTs(tsKvEntry.getTs());
long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
List<ResultSetFuture> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY);
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");
}
UUID uid = entityId.getId();
long partitionTs = toPartitionTs(tsKvEntry.getTs());
long partitionTs = timeseriesDao.toPartitionTs(tsKvEntry.getTs());
saveAndRegisterFutures(futures, entityType, tsKvEntry, uid, partitionTs);
}
return Futures.allAsList(futures);
@ -144,14 +125,6 @@ public class BaseTimeseriesService implements TimeseriesService {
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) {
Validator.validateString(entityType, "Incorrect entityType " + entityType);
Validator.validateId(entityId, "Incorrect entityId " + entityId);
@ -162,6 +135,8 @@ public class BaseTimeseriesService implements TimeseriesService {
throw new IncorrectParameterException("TsKvQuery can't be null");
} else if (isBlank(query.getKey())) {
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.Row;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
@ -30,7 +31,9 @@ import java.util.UUID;
*/
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);

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.Row;
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.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvQuery;
@ -32,8 +33,7 @@ import java.util.Set;
*/
public interface TimeseriesService {
//TODO: Replace this with async operation
List<TsKvEntry> find(String entityType, UUIDBased entityId, TsKvQuery query);
ListenableFuture<List<TsKvEntry>> findAll(String entityType, UUIDBased entityId, List<TsKvQuery> queries);
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.List;
import java.util.Optional;
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
@ -54,8 +55,9 @@ public class BaseAttributesServiceTest extends AbstractServiceTest {
KvEntry attrValue = new StringDataEntry("attribute1", "value1");
AttributeKvEntry attr = new BaseAttributeKvEntry(attrValue, 42L);
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attr)).get();
AttributeKvEntry saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attr.getKey());
Assert.assertEquals(attr, saved);
Optional<AttributeKvEntry> saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attr.getKey()).get();
Assert.assertTrue(saved.isPresent());
Assert.assertEquals(attr, saved.get());
}
@Test
@ -65,15 +67,17 @@ public class BaseAttributesServiceTest extends AbstractServiceTest {
AttributeKvEntry attrOld = new BaseAttributeKvEntry(attrOldValue, 42L);
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrOld)).get();
AttributeKvEntry saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey());
Assert.assertEquals(attrOld, saved);
Optional<AttributeKvEntry> saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey()).get();
Assert.assertTrue(saved.isPresent());
Assert.assertEquals(attrOld, saved.get());
KvEntry attrNewValue = new StringDataEntry("attribute1", "value2");
AttributeKvEntry attrNew = new BaseAttributeKvEntry(attrNewValue, 73L);
attributesService.save(deviceId, DataConstants.CLIENT_SCOPE, Collections.singletonList(attrNew)).get();
saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey());
Assert.assertEquals(attrNew, saved);
saved = attributesService.find(deviceId, DataConstants.CLIENT_SCOPE, attrOld.getKey()).get();
Assert.assertEquals(attrNew, saved.get());
}
@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(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.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 BOOLEAN_KEY = "booleanKey";
public static final int PARTITION_MINUTES = 1100;
private static final long TS = 42L;
KvEntry stringKvEntry = new StringDataEntry(STRING_KEY, "value");
@ -103,27 +101,101 @@ public class TimeseriesServiceTest extends AbstractServiceTest {
}
@Test
public void testFindDeviceTsDataByQuery() throws Exception {
public void testFindDeviceTsData() throws Exception {
DeviceId deviceId = new DeviceId(UUIDs.timeBased());
LocalDateTime localDateTime = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(PARTITION_MINUTES);
log.debug("Start event time is {}", localDateTime);
List<TsKvEntry> entries = new ArrayList<>(PARTITION_MINUTES);
for (int i = 0; i < PARTITION_MINUTES; i++) {
long time = localDateTime.plusMinutes(i).toInstant(ZoneOffset.UTC).toEpochMilli();
BasicTsKvEntry tsKvEntry = new BasicTsKvEntry(time, stringKvEntry);
tsService.save(DataConstants.DEVICE, deviceId, tsKvEntry).get();
entries.add(tsKvEntry);
}
log.debug("Saved all records {}", localDateTime);
List<TsKvEntry> list = tsService.find(DataConstants.DEVICE, deviceId, new BaseTsKvQuery(STRING_KEY, entries.get(599).getTs(),
LocalDateTime.now(ZoneOffset.UTC).toInstant(ZoneOffset.UTC).toEpochMilli()));
log.debug("Fetched records {}", localDateTime);
List<TsKvEntry> expected = entries.subList(600, PARTITION_MINUTES);
assertEquals(expected.size(), list.size());
assertEquals(expected, list);
List<TsKvEntry> entries = new ArrayList<>();
entries.add(save(deviceId, 5000, 100));
entries.add(save(deviceId, 15000, 200));
entries.add(save(deviceId, 25000, 300));
entries.add(save(deviceId, 35000, 400));
entries.add(save(deviceId, 45000, 500));
entries.add(save(deviceId, 55000, 600));
List<TsKvEntry> list = tsService.findAll(DataConstants.DEVICE, deviceId, Collections.singletonList(new BaseTsKvQuery(LONG_KEY, 0,
60000, 20000, 3, Aggregation.NONE))).get();
assertEquals(3, list.size());
assertEquals(55000, list.get(0).getTs());
assertEquals(java.util.Optional.of(600L), list.get(0).getLongValue());
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 {
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.max_limit_per_request=1000
cassandra.query.min_aggregation_step_ms=100

4
docker/docker-compose.yml

@ -18,7 +18,7 @@ version: '2'
services:
thingsboard:
image: "thingsboard/application:1.1.0"
image: "thingsboard/application:1.2.0"
ports:
- "8080:8080"
- "1883:1883"
@ -27,7 +27,7 @@ services:
- thingsboard.env
entrypoint: ./run_thingsboard.sh
thingsboard-db-schema:
image: "thingsboard/thingsboard-db-schema:1.1.0"
image: "thingsboard/thingsboard-db-schema:1.2.0"
env_file:
- thingsboard-db-schema.env
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/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 push thingsboard/thingsboard-db-schema:1.1.0
docker push thingsboard/thingsboard-db-schema:1.2.0
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
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 push thingsboard/application:1.1.0
docker push thingsboard/application:1.2.0
docker push thingsboard/application:latest

2
extensions-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<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;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import java.util.*;
@ -65,4 +67,28 @@ public class DeviceAttributes {
public Optional<AttributeKvEntry> getServerPublicAttribute(String 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;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKey;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import java.util.List;
import java.util.Set;
/**
* @author Andrew Shvayka
*/
@ToString
@AllArgsConstructor
public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg {
@Getter private final TenantId tenantId;
@Getter private final DeviceId deviceId;
@Getter private final Set<AttributeKey> keys;
@Getter private final boolean deleted;
@Getter
private final TenantId tenantId;
@Getter
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) {
return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, false);
public static DeviceAttributesEventNotificationMsg onUpdate(TenantId tenantId, DeviceId deviceId, String scope, List<AttributeKvEntry> values) {
return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, null, scope, values, false);
}
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);
boolean checkAccess(DeviceId deviceId);
void checkAccess(DeviceId deviceId, PluginCallback<Void> callback);
Optional<PluginApiCallSecurityContext> getSecurityCtx();
@ -82,7 +82,7 @@ public interface PluginContext {
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);
@ -92,15 +92,19 @@ public interface PluginContext {
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);
}

2
extensions-core/pom.xml

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

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;
import com.sun.javafx.collections.MappingChange;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.*;
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.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler;
import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler;
@ -66,28 +68,49 @@ public class SubscriptionManager {
DeviceId deviceId = subscription.getDeviceId();
log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), deviceId, address);
registerSubscription(sessionId, deviceId, subscription);
List<TsKvEntry> missedUpdates = new ArrayList<>();
if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
subscription.getKeyStates().entrySet().forEach(e -> {
Optional<AttributeKvEntry> latestOpt = ctx.loadAttribute(deviceId, DataConstants.CLIENT_SCOPE, e.getKey());
if (latestOpt.isPresent()) {
AttributeKvEntry latestEntry = latestOpt.get();
if (latestEntry.getLastUpdateTs() > e.getValue()) {
missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry));
}
final Map<String, Long> keyStates = subscription.getKeyStates();
ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> values) {
List<TsKvEntry> missedUpdates = new ArrayList<>();
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) {
long curTs = System.currentTimeMillis();
List<TsKvQuery> queries = new ArrayList<>();
subscription.getKeyStates().entrySet().forEach(e -> {
TsKvQuery query = new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs);
missedUpdates.addAll(ctx.loadTimeseries(deviceId, query));
queries.add(new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs));
});
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) {

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;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Andrew Shvayka
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GetHistoryCmd implements TelemetryPluginCmd {
private int cmdId;
@ -25,46 +32,8 @@ public class GetHistoryCmd implements TelemetryPluginCmd {
private String keys;
private long startTs;
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;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
@NoArgsConstructor
@AllArgsConstructor
@Data
public abstract class SubscriptionCmd implements TelemetryPluginCmd {
private int cmdId;
@ -31,46 +33,6 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
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
public String toString() {
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;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
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
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
private long startTs;
private long timeWindow;
public long getTimeWindow() {
return timeWindow;
}
public void setTimeWindow(long timeWindow) {
this.timeWindow = timeWindow;
}
private long interval;
private int limit;
private String agg;
@Override
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")) {
List<AttributeKvEntry> attributes;
PluginCallback<List<AttributeKvEntry>> callback = getAttributeKeysPluginCallback(msg);
if (!StringUtils.isEmpty(scope)) {
attributes = ctx.loadAttributes(deviceId, scope);
ctx.loadAttributes(deviceId, scope, callback);
} else {
attributes = new ArrayList<>();
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(ctx.loadAttributes(deviceId, s)));
ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
}
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")) {
if ("timeseries".equals(entity)) {
String keys = request.getParameter("keys");
String keysStr = request.getParameter("keys");
Optional<Long> startTs = request.getLongParamValue("startTs");
Optional<Long> endTs = request.getLongParamValue("endTs");
Optional<Long> interval = request.getLongParamValue("interval");
Optional<Integer> limit = request.getIntParamValue("limit");
Map<String, List<TsData>> data = new LinkedHashMap<>();
for (String key : keys.split(",")) {
List<TsKvEntry> entries = ctx.loadTimeseries(deviceId, new BaseTsKvQuery(key, startTs, endTs, limit));
data.put(key, entries.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList()));
}
msg.getResponseHolder().setResult(new ResponseEntity<>(data, HttpStatus.OK));
Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
List<String> keys = Arrays.asList(keysStr.split(","));
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>>() {
@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)) {
String keys = request.getParameter("keys", "");
List<AttributeKvEntry> attributes;
PluginCallback<List<AttributeKvEntry>> callback = getAttributeValuesPluginCallback(msg);
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 {
attributes = new ArrayList<>();
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> attributes.addAll(getAttributeKvEntries(ctx, s, deviceId, keys)));
if (!StringUtils.isEmpty(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 {
@ -146,7 +165,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
}
});
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
public void onSuccess(PluginContext ctx, Void value) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
@ -155,6 +174,7 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
@Override
public void onFailure(PluginContext ctx, Exception e) {
log.error("Failed to save attributes", e);
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}
});
@ -163,8 +183,8 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
}
}
}
} catch (IOException e) {
log.debug("Failed to process POST request due to IO exception", e);
} catch (IOException | RuntimeException e) {
log.debug("Failed to process POST request due to exception", e);
}
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
}
@ -183,8 +203,18 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
String keysParam = request.getParameter("keys");
if (!StringUtils.isEmpty(keysParam)) {
String[] keys = keysParam.split(",");
ctx.removeAttributes(deviceId, scope, Arrays.asList(keys));
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
ctx.removeAttributes(ctx.getSecurityCtx().orElseThrow(() -> new IllegalArgumentException()).getTenantId(), deviceId, scope, Arrays.asList(keys), new PluginCallback<Void>() {
@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;
}
}
@ -195,14 +225,37 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
}
private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, String scope, DeviceId deviceId, String keysParam) {
List<AttributeKvEntry> attributes;
if (!StringUtils.isEmpty(keysParam)) {
String[] keys = keysParam.split(",");
attributes = ctx.loadAttributes(deviceId, scope, Arrays.asList(keys));
} else {
attributes = ctx.loadAttributes(deviceId, scope);
}
return attributes;
private PluginCallback<List<AttributeKvEntry>> getAttributeKeysPluginCallback(final PluginRestMsg msg) {
return new PluginCallback<List<AttributeKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<AttributeKvEntry> attributes) {
List<String> keys = attributes.stream().map(attrKv -> attrKv.getKey()).collect(Collectors.toList());
msg.getResponseHolder().setResult(new ResponseEntity<>(keys, 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));
}
};
}
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;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId;
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.stream.Collectors;
@Slf4j
public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
private final SubscriptionManager subscriptionManager;
@ -49,27 +51,36 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
public void handleGetAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, GetAttributesRequestRuleToPluginMsg msg) {
GetAttributesRequest request = msg.getPayload();
List<AttributeKvEntry> clientAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getClientAttributeNames());
List<AttributeKvEntry> sharedAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.SHARED_SCOPE, request.getSharedAttributeNames());
BiPluginCallBack<List<AttributeKvEntry>, List<AttributeKvEntry>> callback = new BiPluginCallBack<List<AttributeKvEntry>, List<AttributeKvEntry>>() {
BasicGetAttributesResponse response = BasicGetAttributesResponse.onSuccess(request.getMsgType(),
request.getRequestId(), BasicAttributeKVMsg.from(clientAttributes, sharedAttributes));
@Override
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) {
List<AttributeKvEntry> attributes;
private void getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Optional<Set<String>> names, PluginCallback<List<AttributeKvEntry>> callback) {
if (names.isPresent()) {
if (!names.get().isEmpty()) {
attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names.get()));
ctx.loadAttributes(deviceId, scope, new ArrayList<>(names.get()), callback);
} else {
attributes = ctx.loadAttributes(deviceId, scope);
ctx.loadAttributes(deviceId, scope, callback);
}
} else {
attributes = Collections.emptyList();
callback.onSuccess(ctx, Collections.emptyList());
}
return attributes;
}
@Override
@ -100,6 +111,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
@Override
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)));
}
});
@ -108,7 +120,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
@Override
public void handleUpdateAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, UpdateAttributesRequestRuleToPluginMsg msg) {
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>() {
@Override
public void onSuccess(PluginContext ctx, Void value) {
@ -127,6 +139,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
@Override
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)));
}
});

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.id.DeviceId;
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.PluginContext;
import org.thingsboard.server.extensions.api.plugins.handlers.DefaultWebsocketMsgHandler;
@ -47,6 +48,8 @@ import java.util.stream.Collectors;
public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
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;
@ -104,37 +107,70 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
SubscriptionState sub;
if (keysOptional.isPresent()) {
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())) {
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 {
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());
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()));
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, true, subState);
subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
}
@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())) {
Arrays.stream(DataConstants.ALL_SCOPES).forEach(s -> data.addAll(ctx.loadAttributes(deviceId, s)));
ctx.loadAttributes(deviceId, Arrays.asList(DataConstants.ALL_SCOPES), callback);
} 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()) {
long startTs;
if (cmd.getTimeWindow() > 0) {
List<TsKvEntry> data = new ArrayList<>();
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());
long endTs = System.currentTimeMillis();
startTs = endTs - cmd.getTimeWindow();
for (String key : keys) {
TsKvQuery query = new BaseTsKvQuery(key, startTs, endTs);
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);
startTs = cmd.getStartTs();
long endTs = cmd.getStartTs() + cmd.getTimeWindow();
List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
ctx.loadTimeseries(deviceId, queries, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
} else {
List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
startTs = System.currentTimeMillis();
log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
ctx.loadLatestTimeseries(deviceId, keys, 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) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
sendWsMsg(ctx, sessionRef, update);
}
});
ctx.loadLatestTimeseries(deviceId, keys, getSubscriptionCallback(sessionRef, cmd, sessionId, deviceId, startTs, keys));
}
} else {
ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
@ -206,8 +214,14 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
@Override
public void onFailure(PluginContext ctx, Exception e) {
SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
"Failed to fetch data!");
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);
}
});
@ -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) {
String sessionId = sessionRef.getSessionId();
WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
@ -239,19 +276,35 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
return;
}
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<TsKvEntry> data = new ArrayList<>();
for (String key : keys) {
TsKvQuery query = new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs());
data.addAll(ctx.loadTimeseries(deviceId, query));
}
sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
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());
ctx.loadTimeseries(deviceId, queries, new PluginCallback<List<TsKvEntry>>() {
@Override
public void onSuccess(PluginContext ctx, List<TsKvEntry> 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) {
@ -282,13 +335,6 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
sendWsMsg(ctx, sessionRef, update);
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;
}

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.Data;
import lombok.Getter;
import org.thingsboard.server.common.data.id.DeviceId;
import java.util.Map;
@ -24,16 +25,37 @@ import java.util.Map;
/**
* @author Andrew Shvayka
*/
@Data
@AllArgsConstructor
public class SubscriptionState {
private final String wsSessionId;
private final int subscriptionId;
private final DeviceId deviceId;
private final SubscriptionType type;
private final boolean allKeys;
private final Map<String, Long> keyStates;
@Getter private final String wsSessionId;
@Getter private final int subscriptionId;
@Getter private final DeviceId deviceId;
@Getter private final SubscriptionType type;
@Getter private final boolean allKeys;
@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
public String toString() {

2
extensions/extension-kafka/pom.xml

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

2
extensions/extension-rabbitmq/pom.xml

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

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

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

2
extensions/pom.xml

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

2
pom.xml

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

2
tools/pom.xml

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

2
transport/coap/pom.xml

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

2
transport/http/pom.xml

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

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<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 {
String deviceName = checkDeviceName(getDeviceName(msg));
GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName);
deviceSessionCtx.setClosed(true);
if (deviceSessionCtx != null) {
deviceSessionCtx.setClosed(true);
}
ack(msg);
}

2
transport/pom.xml

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

10
ui/package.json

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

2
ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>1.1.1-SNAPSHOT</version>
<version>1.2.0-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<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>
</md-card-title-text>
</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>
<md-card-content>
<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>
</md-card-title-text>
</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>
<md-card-content>
<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 = {
assignDashboardToCustomer: assignDashboardToCustomer,
getCustomerDashboards: getCustomerDashboards,
getServerTimeDiff: getServerTimeDiff,
getDashboard: getDashboard,
getTenantDashboards: getTenantDashboards,
deleteDashboard: deleteDashboard,
@ -71,6 +72,21 @@ function DashboardService($http, $q) {
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) {
var deferred = $q.defer();
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 thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service';
import DataAggregator from './data-aggregator';
export default angular.module('thingsboard.api.datasource', [thingsboardApiDevice, thingsboardApiTelemetryWebsocket, thingsboardTypes, thingsboardUtils])
.factory('datasourceService', DatasourceService)
.name;
/*@ngInject*/
function DatasourceService($timeout, $log, telemetryWebsocketService, types, utils) {
function DatasourceService($timeout, $filter, $log, telemetryWebsocketService, types, utils) {
var subscriptions = {};
@ -73,7 +74,7 @@ function DatasourceService($timeout, $log, telemetryWebsocketService, types, uti
subscription = subscriptions[listener.datasourceSubscriptionKey];
subscription.syncListener(listener);
} else {
subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $log, types, utils);
subscription = new DatasourceSubscription(datasourceSubscription, telemetryWebsocketService, $timeout, $filter, $log, types, utils);
subscriptions[listener.datasourceSubscriptionKey] = subscription;
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 datasourceType = datasourceSubscription.datasourceType;
@ -107,9 +108,9 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
datasourceSubscription.subscriptionTimewindow.fixedWindow;
var realtime = datasourceSubscription.subscriptionTimewindow &&
datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
var dataGenFunction = null;
var timer;
var frequency;
var dataAggregator;
var subscription = {
addListener: addListener,
@ -130,42 +131,42 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
dataKey.index = i;
var key;
if (datasourceType === types.datasourceType.function) {
key = utils.objectHashCode(dataKey);
if (!dataKey.func) {
dataKey.func = new Function("time", "prevValue", dataKey.funcBody);
}
datasourceData[key] = [];
dataKeys[key] = dataKey;
} else if (datasourceType === types.datasourceType.device) {
key = dataKey.name + '_' + dataKey.type;
} else {
if (dataKey.postFuncBody && !dataKey.postFunc) {
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];
if (!dataKeysList) {
dataKeysList = [];
dataKeys[key] = dataKeysList;
}
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;
}
if (datasourceType === types.datasourceType.function) {
frequency = 1000;
if (datasourceSubscription.type === types.widgetType.timeseries.value) {
dataGenFunction = generateSeries;
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;
frequency = Math.min(datasourceSubscription.subscriptionTimewindow.aggregation.interval, 5000);
}
}
}
@ -188,14 +189,7 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
function syncListener(listener) {
var key;
var dataKey;
if (datasourceType === types.datasourceType.function) {
for (key in dataKeys) {
dataKey = dataKeys[key];
listener.dataUpdated(datasourceData[key],
listener.datasourceIndex,
dataKey.index);
}
} else if (datasourceType === types.datasourceType.device) {
if (datasourceType === types.datasourceType.device || datasourceSubscription.type === types.widgetType.timeseries.value) {
for (key in dataKeys) {
var dataKeysList = dataKeys[key];
for (var i = 0; i < dataKeysList.length; i++) {
@ -203,9 +197,16 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
var datasourceKey = key + '_' + i;
listener.dataUpdated(datasourceData[datasourceKey],
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()) {
return;
}
//$log.debug("started!");
var subsTw = datasourceSubscription.subscriptionTimewindow;
var tsKeyNames = [];
var dataKey;
if (datasourceType === types.datasourceType.device) {
//send subscribe command
@ -223,12 +227,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
for (var key in dataKeys) {
var dataKeysList = dataKeys[key];
var dataKey = dataKeysList[0];
dataKey = dataKeysList[0];
if (dataKey.type === types.dataKeyType.timeseries) {
if (tsKeys.length > 0) {
tsKeys += ',';
}
tsKeys += dataKey.name;
tsKeyNames.push(dataKey.name);
} else if (dataKey.type === types.dataKeyType.attribute) {
if (attrKeys.length > 0) {
attrKeys += ',';
@ -247,19 +252,22 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
var historyCommand = {
deviceId: datasourceSubscription.deviceId,
keys: tsKeys,
startTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.startTimeMs,
endTs: datasourceSubscription.subscriptionTimewindow.fixedWindow.endTimeMs
startTs: subsTw.fixedWindow.startTimeMs,
endTs: subsTw.fixedWindow.endTimeMs,
interval: subsTw.aggregation.interval,
limit: subsTw.aggregation.limit,
agg: subsTw.aggregation.type
};
subscriber = {
historyCommand: historyCommand,
type: types.dataKeyType.timeseries,
onData: function (data) {
onData(data, types.dataKeyType.timeseries);
if (data.data) {
onData(data.data, types.dataKeyType.timeseries, null, null, true);
}
},
onReconnected: function() {
onReconnected();
}
onReconnected: function() {}
};
telemetryWebsocketService.subscribe(subscriber);
@ -272,21 +280,39 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
keys: tsKeys
};
if (datasourceSubscription.type === types.widgetType.timeseries.value) {
subscriptionCommand.timeWindow = datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
}
subscriber = {
subscriptionCommand: subscriptionCommand,
type: types.dataKeyType.timeseries,
onData: function (data) {
onData(data, types.dataKeyType.timeseries);
},
onReconnected: function() {
onReconnected();
}
type: types.dataKeyType.timeseries
};
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);
subscribers[subscriber.subscriptionCommand.cmdId] = subscriber;
@ -304,11 +330,11 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
subscriptionCommand: subscriptionCommand,
type: types.dataKeyType.attribute,
onData: function (data) {
onData(data, types.dataKeyType.attribute);
if (data.data) {
onData(data.data, types.dataKeyType.attribute, null, null, true);
}
},
onReconnected: function() {
onReconnected();
}
onReconnected: function() {}
};
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) {
onTick();
onTick(false);
} 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() {
@ -332,35 +396,25 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
}
if (datasourceType === types.datasourceType.device) {
for (var cmdId in subscribers) {
telemetryWebsocketService.unsubscribe(subscribers[cmdId]);
var subscriber = subscribers[cmdId];
telemetryWebsocketService.unsubscribe(subscriber);
if (subscriber.onDestroy) {
subscriber.onDestroy();
}
}
subscribers = {};
}
//$log.debug("unsibscribed!");
}
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);
}
if (dataAggregator) {
dataAggregator.destroy();
dataAggregator = null;
}
return data;
}
function generateSeries(dataKey, startTime, endTime) {
function generateSeries(dataKey, index, startTime, endTime) {
var data = [];
var prevSeries;
var datasourceKeyData = datasourceData[dataKey.key];
var datasourceDataKey = dataKey.key + '_' + index;
var datasourceKeyData = datasourceData[datasourceDataKey].data;
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else {
@ -377,23 +431,12 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
if (data.length > 0) {
dataKey.lastUpdateTime = data[data.length - 1][0];
}
if (realtime) {
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);
}
return data;
}
function generateLatest(dataKey) {
function generateLatest(dataKey, apply) {
var prevSeries;
var datasourceKeyData = datasourceData[dataKey.key];
var datasourceKeyData = datasourceData[dataKey.key].data;
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else {
@ -404,64 +447,54 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
series.push(time);
var value = dataKey.func(time, prevSeries[1]);
series.push(value);
datasourceData[dataKey.key] = [series];
datasourceData[dataKey.key].data = [series];
for (var i in listeners) {
var listener = listeners[i];
listener.dataUpdated(datasourceData[dataKey.key],
listener.datasourceIndex,
dataKey.index);
dataKey.index, apply);
}
}
function onTick() {
function onTick(apply) {
var key;
if (datasourceSubscription.type === types.widgetType.timeseries.value) {
var startTime;
var endTime;
var generatedData = {
data: {
}
};
for (key in dataKeys) {
var dataKey = dataKeys[key];
if (!startTime) {
if (realtime) {
endTime = (new Date).getTime();
if (dataKey.lastUpdateTime) {
startTime = dataKey.lastUpdateTime + frequency;
var dataKeyList = dataKeys[key];
for (var index = 0; index < dataKeyList.length; index ++) {
var dataKey = dataKeyList[index];
if (!startTime) {
if (realtime) {
if (dataKey.lastUpdateTime) {
startTime = dataKey.lastUpdateTime + frequency
} else {
startTime = datasourceSubscription.subscriptionTimewindow.startTs;
}
endTime = startTime + datasourceSubscription.subscriptionTimewindow.realtimeWindowMs;
} 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) {
for (key in dataKeys) {
generateLatest(dataKeys[key]);
generateLatest(dataKeys[key], apply);
}
}
if (!history) {
timer = $timeout(onTick, 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);
}
}
}
timer = $timeout(function() {onTick(true)}, frequency / 2, false);
}
}
@ -477,18 +510,23 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
}
}
function onData(sourceData, type) {
function onData(sourceData, type, startTs, endTs, apply) {
for (var keyName in sourceData) {
var keyData = sourceData[keyName];
var key = keyName + '_' + type;
var dataKeyList = dataKeys[key];
for (var keyIndex = 0; keyIndex < dataKeyList.length; keyIndex++) {
var datasourceKey = key + "_" + keyIndex;
if (datasourceData[datasourceKey]) {
if (datasourceData[datasourceKey].data) {
var dataKey = dataKeyList[keyIndex];
var data = [];
var prevSeries;
var datasourceKeyData = datasourceData[datasourceKey];
var datasourceKeyData;
if (realtime) {
datasourceKeyData = [];
} else {
datasourceKeyData = datasourceData[datasourceKey].data;
}
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else {
@ -519,17 +557,13 @@ function DatasourceSubscription(datasourceSubscription, telemetryWebsocketServic
data.push(series);
}
}
if (data.length > 0) {
if (realtime) {
datasourceData[datasourceKey] = boundToInterval(datasourceKeyData.concat(data), datasourceSubscription.subscriptionTimewindow.realtimeWindowMs);
} else {
datasourceData[datasourceKey] = data;
}
if (data || (startTs && endTs)) {
datasourceData[datasourceKey].data = data;
for (var i2 in listeners) {
var listener = listeners[i2];
listener.dataUpdated(datasourceData[datasourceKey],
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;
/*@ngInject*/
function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
function DeviceService($http, $q, $filter, userService, telemetryWebsocketService, types) {
var deviceAttributesSubscriptionMap = {};
@ -30,6 +30,9 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
deleteDevice: deleteDevice,
getCustomerDevices: getCustomerDevices,
getDevice: getDevice,
getDevices: getDevices,
processDeviceAliases: processDeviceAliases,
checkDeviceAlias: checkDeviceAlias,
getDeviceCredentials: getDeviceCredentials,
getDeviceKeys: getDeviceKeys,
getDeviceTimeseriesValues: getDeviceTimeseriesValues,
@ -99,6 +102,200 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
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) {
var deferred = $q.defer();
var url = '/api/device';
@ -212,7 +409,7 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
return deferred.promise;
}
function processDeviceAttributes(attributes, query, deferred, successCallback, update) {
function processDeviceAttributes(attributes, query, deferred, successCallback, update, apply) {
attributes = $filter('orderBy')(attributes, query.order);
if (query.search != null) {
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);
responseData.data = attributes.slice(startIndex, startIndex + query.limit);
successCallback(responseData, update);
successCallback(responseData, update, apply);
if (deferred) {
deferred.resolve();
}
@ -236,13 +433,13 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
if (das.attributes) {
processDeviceAttributes(das.attributes, query, deferred, successCallback);
das.subscriptionCallback = function(attributes) {
processDeviceAttributes(attributes, query, null, successCallback, true);
processDeviceAttributes(attributes, query, null, successCallback, true, true);
}
} else {
das.subscriptionCallback = function(attributes) {
processDeviceAttributes(attributes, query, deferred, successCallback);
processDeviceAttributes(attributes, query, deferred, successCallback, false, true);
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,
type: type,
onData: function (data) {
onSubscriptionData(data, subscriptionId);
if (data.data) {
onSubscriptionData(data.data, subscriptionId);
}
}
};
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);
if (data.subscriptionId) {
var subscriber = subscribers[data.subscriptionId];
if (subscriber && data.data) {
subscriber.onData(data.data);
if (subscriber && 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();
}
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 () {
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;
/*@ngInject*/
function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
function UserService($http, $q, $rootScope, store, jwtHelper, $translate, $state) {
var currentUser = null,
currentUserDetails = null,
userLoaded = false;
var refreshTokenQueue = [];
@ -47,6 +48,8 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
refreshJwtToken: refreshJwtToken,
refreshTokenPending: refreshTokenPending,
updateAuthorizationHeader: updateAuthorizationHeader,
gotoDefaultPlace: gotoDefaultPlace,
forceDefaultPlace: forceDefaultPlace,
logout: logout
}
@ -86,6 +89,7 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
function setUserFromJwtToken(jwtToken, refreshToken, notify, doLogout) {
currentUser = null;
currentUserDetails = null;
if (!jwtToken) {
clearTokenData();
if (notify) {
@ -222,7 +226,25 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
} else if (currentUser) {
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() {
deferred.reject();
});
@ -331,4 +353,40 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate) {
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 TbFlot from '../widget/lib/flot-widget';
import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-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 '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) {
$window.$ = $;
$window.jQuery = $;
$window.moment = moment;
$window.tinycolor = tinycolor;
$window.lazyLoad = $ocLazyLoad;
$window.TbFlot = TbFlot;
$window.TbAnalogueLinearGauge = TbAnalogueLinearGauge;
$window.TbAnalogueRadialGauge = TbAnalogueRadialGauge;
$window.TbDigitalGauge = TbDigitalGauge;
$window.TbCanvasDigitalGauge = TbCanvasDigitalGauge;
$window.TbMapWidget = TbMapWidget;
$window.cssjs = cssjs;
@ -57,6 +60,8 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
var editingWidgetType;
var widgetsInfoInMemoryCache = {};
var widgetsTypeFunctionsInMemoryCache = {};
var widgetsInfoFetchQueue = {};
var allWidgetsBundles = undefined;
var systemWidgetsBundles = undefined;
@ -83,6 +88,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
deleteWidgetsBundle: deleteWidgetsBundle,
getBundleWidgetTypes: getBundleWidgetTypes,
getWidgetInfo: getWidgetInfo,
getWidgetTypeFunction: getWidgetTypeFunction,
getInstantWidgetInfo: getInstantWidgetInfo,
deleteWidgetType: deleteWidgetType,
saveWidgetType: saveWidgetType,
@ -123,7 +129,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
resources: [],
templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-not-found</div></div>',
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',
dataKeySettingsSchema: '{}\n',
defaultConfig: '{\n' +
@ -141,7 +147,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
resources: [],
templateHtml: '<div class="tb-widget-error-container"><div translate class="tb-widget-error-msg">widget.widget-type-load-error</div>',
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',
dataKeySettingsSchema: '{}\n',
defaultConfig: '{\n' +
@ -228,19 +234,34 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
/** Cache functions **/
function createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem) {
return (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
}
function getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) {
var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
return widgetsInfoInMemoryCache[key];
}
function getWidgetTypeFunctionFromCache(bundleAlias, widgetTypeAlias, isSystem) {
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
return widgetsTypeFunctionsInMemoryCache[key];
}
function putWidgetInfoToCache(widgetInfo, bundleAlias, widgetTypeAlias, isSystem) {
var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
widgetsInfoInMemoryCache[key] = widgetInfo;
}
function putWidgetTypeFunctionToCache(widgetTypeFunction, bundleAlias, widgetTypeAlias, isSystem) {
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
widgetsTypeFunctionsInMemoryCache[key] = widgetTypeFunction;
}
function deleteWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem) {
var key = (isSystem ? 'sys_' : '') + bundleAlias + '_' + widgetTypeAlias;
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
delete widgetsInfoInMemoryCache[key];
delete widgetsTypeFunctionsInMemoryCache[key];
}
function deleteWidgetsBundleFromCache(bundleAlias, isSystem) {
@ -248,6 +269,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
for (var cacheKey in widgetsInfoInMemoryCache) {
if (cacheKey.startsWith(key)) {
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) {
var deferred = $q.defer();
var widgetInfo = getWidgetInfoFromCache(bundleAlias, widgetTypeAlias, isSystem);
@ -465,32 +497,130 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if ($rootScope.widgetEditMode) {
loadWidget(editingWidgetType, bundleAlias, isSystem, deferred);
} else {
getWidgetType(bundleAlias, widgetTypeAlias, isSystem).then(
function success(widgetType) {
loadWidget(widgetType, bundleAlias, isSystem, deferred);
}, function fail() {
deferred.resolve(missingWidgetType);
}
);
var key = createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem);
var fetchQueue = widgetsInfoFetchQueue[key];
if (fetchQueue) {
fetchQueue.push(deferred);
} 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;
}
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) {
var widgetInfo = toWidgetInfo(widgetType);
var key = createWidgetInfoCacheKey(bundleAlias, widgetInfo.alias, isSystem);
loadWidgetResources(widgetInfo, bundleAlias, isSystem).then(
function success() {
putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
deferred.resolve(widgetInfo);
}, function fail(errorMessages) {
widgetInfo = angular.copy(errorWidgetType);
for (var e in errorMessages) {
var error = errorMessages[e];
widgetInfo.templateHtml += '<div class="tb-widget-error-msg">' + error + '</div>';
var widgetType = null;
try {
widgetType = createWidgetTypeFunction(widgetInfo, key);
} catch (e) {
var details = utils.parseException(e);
var errorMessage = 'Failed to compile widget script. \n Error: ' + details.message;
processWidgetLoadError([errorMessage], key, deferred);
}
widgetInfo.templateHtml += '</div>';
deferred.resolve(widgetInfo);
if (widgetType) {
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,
$httpProvider,
$translateProvider,
storeProvider) {
storeProvider,
locales) {
injectTapEventPlugin();
$locationProvider.html5Mode(true);
$urlRouterProvider.otherwise('/home');
//$urlRouterProvider.otherwise('/home');
storeProvider.setCaching(false);
$translateProvider.useStaticFilesLoader({
prefix: 'static/locale/',// path to translations files
suffix: '.json'// suffix, currently- extension of the translations
});
$translateProvider.useSanitizeValueStrategy('sanitize');
$translateProvider.preferredLanguage('en_US');
$translateProvider.useLocalStorage();
$translateProvider.useMissingTranslationHandlerLog();
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
for (var langKey in locales) {
var translationTable = locales[langKey];
$translateProvider.translations(langKey, translationTable);
}
$httpProvider.interceptors.push('globalInterceptor');
$provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate, $injector) {
$provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate/*, $injector*/) {
return function (exception, cause) {
var rootScope = $injector.get("$rootScope");
/* var rootScope = $injector.get("$rootScope");
var $window = $injector.get("$window");
var utils = $injector.get("utils");
if (rootScope.widgetEditMode) {
@ -63,7 +64,7 @@ export default function AppConfig($provide,
var data = utils.parseException(exception);
parentScope.$emit('widgetException', data);
parentScope.$apply();
}
}*/
$delegate(exception, cause);
};
}]);
@ -141,7 +142,7 @@ export default function AppConfig($provide,
.backgroundPalette('tb-primary');
$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 '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import thingsboardLocales from './locale/locale.constant';
import thingsboardLogin from './login';
import thingsboardDialogs from './components/datakey-config-dialog.controller';
import thingsboardMenu from './services/menu.service';
import thingsboardRaf from './common/raf.provider';
import thingsboardUtils from './common/utils.service';
import thingsboardTypes from './common/types.constant';
import thingsboardApiTime from './api/time.service';
import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter';
import thingsboardHelp from './help/help.directive';
import thingsboardToast from './services/toast';
@ -57,6 +60,7 @@ import thingsboardApiLogin from './api/login.service';
import thingsboardApiDevice from './api/device.service';
import thingsboardApiUser from './api/user.service';
import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css';
import 'angular-material/angular-material.min.css';
import 'angular-material-icons/angular-material-icons.css';
@ -91,11 +95,14 @@ angular.module('thingsboard', [
'ngclipboard',
react.name,
'flow',
thingsboardLocales,
thingsboardLogin,
thingsboardDialogs,
thingsboardMenu,
thingsboardRaf,
thingsboardUtils,
thingsboardTypes,
thingsboardApiTime,
thingsboardKeyboardShortcut,
thingsboardHelp,
thingsboardToast,

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

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

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

@ -22,7 +22,7 @@ export default angular.module('thingsboard.utils', [thingsboardTypes])
.name;
/*@ngInject*/
function Utils($mdColorPalette, types) {
function Utils($mdColorPalette, $rootScope, $window, types) {
var predefinedFunctions = {},
predefinedFunctionsList = [],
@ -102,6 +102,7 @@ function Utils($mdColorPalette, types) {
genMaterialColor: genMaterialColor,
objectHashCode: objectHashCode,
parseException: parseException,
processWidgetException: processWidgetException,
isDescriptorSchemaNotEmpty: isDescriptorSchemaNotEmpty,
filterSearchTextEntities: filterSearchTextEntities
}
@ -147,7 +148,7 @@ function Utils($mdColorPalette, types) {
return hash;
}
function parseException(exception) {
function parseException(exception, lineOffset) {
var data = {};
if (exception) {
if (angular.isString(exception) || exception instanceof String) {
@ -170,7 +171,10 @@ function Utils($mdColorPalette, types) {
var lineInfoRegexp = /(.*<anonymous>):(\d*)(:)?(\d*)?/g;
var lineInfoGroups = lineInfoRegexp.exec(exception.stack);
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) {
data.columnNumber = lineInfoGroups[4];
}
@ -181,6 +185,16 @@ function Utils($mdColorPalette, types) {
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) {
var datasource = angular.copy(defaultDatasource);
if (angular.isDefined(dataKeySchema)) {

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

@ -27,7 +27,7 @@
</md-button>
</div>
</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>
<md-dialog-content>
<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.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) {
var pageLink = {limit: 10, textSearch: searchText};
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);
}, function fail() {
deferred.reject();
@ -79,6 +71,8 @@ function DashboardSelect($compile, $templateCache, $q, dashboardService, userSer
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
scope.dashboard = ngModelCtrl.$viewValue;
} else {
scope.dashboard = null;
}
}
@ -106,6 +100,7 @@ function DashboardSelect($compile, $templateCache, $q, dashboardService, userSer
link: linker,
scope: {
dashboardsScope: '@',
customerId: '=',
theForm: '=?',
tbRequired: '=?',
selectFirstDashboard: '='

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

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

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

@ -51,7 +51,7 @@ div.tb-widget {
height: 32px;
min-width: 32px;
min-height: 32px;
md-icon {
md-icon, ng-md-icon {
width: 20px;
height: 20px;
min-width: 20px;
@ -93,6 +93,10 @@ md-content.tb-dashboard-content {
right: 0;
bottom: 0;
outline: none;
background: none;
.gridster-item {
@include transition(none);
}
}
.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"
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-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)">
<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">
<ul>
<!-- 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>
<div tb-expand-fullscreen
fullscreen-background-style="vm.dashboardStyle"
expand-button-id="expand-button"
expand-button-size="20"
on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)"
layout="column"
class="tb-widget"
@ -45,55 +46,55 @@
color: vm.widgetColor(widget),
backgroundColor: vm.widgetBackgroundColor(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>
<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 class="tb-widget-actions" layout="row" layout-align="start center">
<md-button id="expand-button"
ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
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"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
class="md-icon-button"
ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.edit' | translate }}
</md-tooltip>
<md-icon class="material-icons">
edit
</md-icon>
<ng-md-icon size="20" icon="edit"></ng-md-icon>
</md-button>
<md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
class="md-icon-button"
ng-click="vm.exportWidget($event, widget)"
aria-label="{{ 'widget.export' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.export' | translate }}
</md-tooltip>
<md-icon class="material-icons">
file_download
</md-icon>
<ng-md-icon size="20" icon="file_download"></ng-md-icon>
</md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
class="md-icon-button"
ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.remove' | translate }}
</md-tooltip>
<md-icon class="material-icons">
close
</md-icon>
<ng-md-icon size="20" icon="close"></ng-md-icon>
</md-button>
</div>
<div flex layout="column" class="tb-widget-content">
<div flex tb-widget
locals="{ visibleRect: vm.visibleRect, widget: widget, deviceAliasList: vm.deviceAliasList, isPreview: vm.isEdit }">
locals="{ visibleRect: vm.visibleRect,
widget: widget,
aliasesInfo: vm.aliasesInfo,
isEdit: vm.isEdit,
stDiff: vm.stDiff,
dashboardTimewindow: vm.dashboardTimewindow,
dashboardTimewindowApi: vm.dashboardTimewindowApi }">
</div>
</div>
</div>

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

@ -26,7 +26,7 @@
</md-button>
</div>
</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>
<md-dialog-content>
<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