Browse Source

Merge pull request #56 from thingsboard/feature/aggregation

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

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) {

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);
}
}

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

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();
}
}
}

34
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;
@ -72,6 +77,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 +126,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);

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) {

3
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;
@ -28,6 +29,8 @@ public interface DeviceService {
Device findDeviceById(DeviceId deviceId);
ListenableFuture<Device> findDeviceByIdAsync(DeviceId deviceId);
Optional<Device> findDeviceByTenantIdAndName(TenantId tenantId, String name);
Device saveDevice(Device device);

11
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;
@ -70,6 +73,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);

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!");
}
}
}

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);
}
}

225
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

@ -91,7 +91,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>

2
ui/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard",
"private": true,
"version": "1.1.0",
"version": "1.2.0",
"description": "Thingsboard UI",
"licenses": [
{

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
}
}
}

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

@ -212,7 +212,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 +222,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 +236,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 +304,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));
}
}
}

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

@ -129,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' +
@ -147,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' +

2
ui/src/app/app.config.js

@ -142,7 +142,7 @@ export default function AppConfig($provide,
.backgroundPalette('tb-primary');
$mdThemingProvider.setDefaultTheme('default');
$mdThemingProvider.alwaysWatchTheme(true);
//$mdThemingProvider.alwaysWatchTheme(true);
}
}

2
ui/src/app/app.js

@ -51,6 +51,7 @@ 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';
@ -101,6 +102,7 @@ angular.module('thingsboard', [
thingsboardRaf,
thingsboardUtils,
thingsboardTypes,
thingsboardApiTime,
thingsboardKeyboardShortcut,
thingsboardHelp,
thingsboardToast,

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"

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">

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

@ -52,6 +52,7 @@ function Dashboard() {
bindToController: {
widgets: '=',
deviceAliasList: '=',
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, $mdMedia, $log, toast, types) {
function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, timeService, types) {
var highlightedMode = false;
var highlightedWidget = null;
@ -94,8 +97,14 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
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,
@ -173,6 +182,21 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
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();
// }
@ -302,7 +326,28 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
});
});
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();
@ -632,7 +677,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
}
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() {
@ -649,6 +699,9 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
function dashboardLoaded() {
$timeout(function () {
$scope.$watch('vm.dashboardTimewindow', function () {
$scope.$broadcast('dashboardTimewindowChanged', vm.dashboardTimewindow);
}, true);
adoptMaxRows();
vm.dashboardLoading = false;
$timeout(function () {

3
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,7 @@ md-content.tb-dashboard-content {
right: 0;
bottom: 0;
outline: none;
background: none;
.gridster-item {
@include transition(none);
}

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.widgetItemMap" 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 button-color="vm.widgetColor(widget)" 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, isEdit: vm.isEdit }">
locals="{ visibleRect: vm.visibleRect,
widget: widget,
deviceAliasList: vm.deviceAliasList,
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"

6
ui/src/app/components/expand-fullscreen.directive.js

@ -101,11 +101,15 @@ function ExpandFullscreen($compile, $document) {
if (attrs.expandButtonId) {
expandButton = $('#' + attrs.expandButtonId, element)[0];
}
var buttonSize;
if (attrs.expandButtonSize) {
buttonSize = attrs.expandButtonSize;
}
var html = '<md-tooltip md-direction="{{expanded ? \'bottom\' : \'top\'}}">' +
'{{(expanded ? \'fullscreen.exit\' : \'fullscreen.expand\') | translate}}' +
'</md-tooltip>' +
'<ng-md-icon icon="{{expanded ? \'fullscreen_exit\' : \'fullscreen\'}}" ' +
'<ng-md-icon ' + (buttonSize ? 'size="'+ buttonSize +'" ' : '') + 'icon="{{expanded ? \'fullscreen_exit\' : \'fullscreen\'}}" ' +
'options=\'{"easing": "circ-in-out", "duration": 375, "rotation": "none"}\'>' +
'</ng-md-icon>';

21
ui/src/app/components/legend-config-button.tpl.html

@ -0,0 +1,21 @@
<!--
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.
-->
<md-button ng-disabled="disabled" class="md-raised md-primary" ng-click="openEditMode($event)">
<ng-md-icon icon="toc"></ng-md-icon>
<span translate>legend.settings</span>
</md-button>

35
ui/src/app/components/legend-config-panel.controller.js

@ -0,0 +1,35 @@
/*
* 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.
*/
/*@ngInject*/
export default function LegendConfigPanelController(mdPanelRef, $scope, types, legendConfig, onLegendConfigUpdate) {
var vm = this;
vm._mdPanelRef = mdPanelRef;
vm.legendConfig = legendConfig;
vm.onLegendConfigUpdate = onLegendConfigUpdate;
vm.positions = types.position;
vm._mdPanelRef.config.onOpenComplete = function () {
$scope.theForm.$setPristine();
}
$scope.$watch('vm.legendConfig', function () {
if (onLegendConfigUpdate) {
onLegendConfigUpdate(vm.legendConfig);
}
}, true);
}

47
ui/src/app/components/legend-config-panel.tpl.html

@ -0,0 +1,47 @@
<!--
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.
-->
<form name="theForm" ng-submit="vm.update()">
<fieldset ng-disabled="loading">
<md-content style="height: 100%" flex layout="column">
<section layout="column">
<md-content class="md-padding" layout="column">
<md-input-container>
<label translate>legend.position</label>
<md-select ng-model="vm.legendConfig.position" style="min-width: 150px;">
<md-option ng-repeat="pos in vm.positions" ng-value="pos.value">
{{pos.name | translate}}
</md-option>
</md-select>
</md-input-container>
<md-checkbox flex aria-label="{{ 'legend.show-min' | translate }}"
ng-model="vm.legendConfig.showMin">{{ 'legend.show-min' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'legend.show-max' | translate }}"
ng-model="vm.legendConfig.showMax">{{ 'legend.show-max' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'legend.show-avg' | translate }}"
ng-model="vm.legendConfig.showAvg">{{ 'legend.show-avg' | translate }}
</md-checkbox>
<md-checkbox flex aria-label="{{ 'legend.show-total' | translate }}"
ng-model="vm.legendConfig.showTotal">{{ 'legend.show-total' | translate }}
</md-checkbox>
</md-content>
</section>
</md-content>
</fieldset>
</form>

151
ui/src/app/components/legend-config.directive.js

@ -0,0 +1,151 @@
/*
* 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.
*/
import './legend-config.scss';
import $ from 'jquery';
/* eslint-disable import/no-unresolved, import/default */
import legendConfigButtonTemplate from './legend-config-button.tpl.html';
import legendConfigPanelTemplate from './legend-config-panel.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import LegendConfigPanelController from './legend-config-panel.controller';
export default angular.module('thingsboard.directives.legendConfig', [])
.controller('LegendConfigPanelController', LegendConfigPanelController)
.directive('tbLegendConfig', LegendConfig)
.name;
/* eslint-disable angular/angularelement */
/*@ngInject*/
function LegendConfig($compile, $templateCache, types, $mdPanel, $document) {
var linker = function (scope, element, attrs, ngModelCtrl) {
/* tbLegendConfig (ng-model)
* {
* position: types.position.bottom.value,
* showMin: false,
* showMax: false,
* showAvg: true,
* showTotal: false
* }
*/
var template = $templateCache.get(legendConfigButtonTemplate);
element.html(template);
scope.openEditMode = function (event) {
if (scope.disabled) {
return;
}
var position;
var panelHeight = 220;
var panelWidth = 220;
var offset = element[0].getBoundingClientRect();
var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
var yPosition;
var xPosition;
if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
yPosition = $mdPanel.yPosition.ABOVE;
} else {
yPosition = $mdPanel.yPosition.BELOW;
}
if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
xPosition = $mdPanel.xPosition.ALIGN_END;
} else {
xPosition = $mdPanel.xPosition.ALIGN_START;
}
position = $mdPanel.newPanelPosition()
.relativeTo(element)
.addPanelPosition(xPosition, yPosition);
var config = {
attachTo: angular.element($document[0].body),
controller: 'LegendConfigPanelController',
controllerAs: 'vm',
templateUrl: legendConfigPanelTemplate,
panelClass: 'tb-legend-config-panel',
position: position,
fullscreen: false,
locals: {
'legendConfig': angular.copy(scope.model),
'onLegendConfigUpdate': function (legendConfig) {
scope.model = legendConfig;
scope.updateView();
}
},
openFrom: event,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false
};
$mdPanel.open(config);
}
scope.updateView = function () {
var value = {};
var model = scope.model;
value.position = model.position;
value.showMin = model.showMin;
value.showMax = model.showMax;
value.showAvg = model.showAvg;
value.showTotal = model.showTotal;
ngModelCtrl.$setViewValue(value);
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
if (!scope.model) {
scope.model = {};
}
var model = scope.model;
model.position = value.position || types.position.bottom.value;
model.showMin = angular.isDefined(value.showMin) ? value.showMin : false;
model.showMax = angular.isDefined(value.showMax) ? value.showMax : false;
model.showAvg = angular.isDefined(value.showAvg) ? value.showAvg : true;
model.showTotal = angular.isDefined(value.showTotal) ? value.showTotal : false;
} else {
scope.model = {
position: types.position.bottom.value,
showMin: false,
showMax: false,
showAvg: true,
showTotal: false
}
}
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
scope: {
disabled:'=ngDisabled'
},
link: linker
};
}
/* eslint-enable angular/angularelement */

49
ui/src/app/components/legend-config.scss

@ -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.
*/
.md-panel {
&.tb-legend-config-panel {
position: absolute;
}
}
.tb-legend-config-panel {
max-height: 220px;
min-width: 220px;
background: white;
border-radius: 4px;
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 13px 19px 2px rgba(0, 0, 0, 0.14),
0 5px 24px 4px rgba(0, 0, 0, 0.12);
overflow: hidden;
form, fieldset {
height: 100%;
}
md-content {
background-color: #fff;
overflow: hidden;
}
.md-padding {
padding: 0 16px;
}
}
tb-legend-config {
span {
pointer-events: all;
cursor: pointer;
}
}

85
ui/src/app/components/legend.directive.js

@ -0,0 +1,85 @@
/*
* 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.
*/
import './legend.scss';
/* eslint-disable import/no-unresolved, import/default */
import legendTemplate from './legend.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
export default angular.module('thingsboard.directives.legend', [])
.directive('tbLegend', Legend)
.name;
/*@ngInject*/
function Legend($compile, $templateCache, types) {
var linker = function (scope, element) {
var template = $templateCache.get(legendTemplate);
element.html(template);
scope.displayHeader = function() {
return scope.legendConfig.showMin === true ||
scope.legendConfig.showMax === true ||
scope.legendConfig.showAvg === true ||
scope.legendConfig.showTotal === true;
}
scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value ||
scope.legendConfig.position === types.position.top.value;
scope.$on('legendDataUpdated', function () {
scope.$digest();
});
scope.toggleHideData = function(index) {
scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden;
scope.$emit('legendDataHiddenChanged', index);
}
$compile(element.contents())(scope);
}
/* scope.legendData = {
keys: [],
data: []
key: {
label: '',
color: ''
dataIndex: 0
}
data: {
min: null,
max: null,
avg: null,
total: null
}
};*/
return {
restrict: "E",
link: linker,
scope: {
legendConfig: '=',
legendData: '='
}
};
}

53
ui/src/app/components/legend.scss

@ -0,0 +1,53 @@
/**
* 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.
*/
table.tb-legend {
width: 100%;
font-size: 12px;
.tb-legend-header, .tb-legend-value {
text-align: right;
}
.tb-legend-header {
th {
color: rgb(255,110,64);
white-space: nowrap;
padding: 0 10px 1px 0;
}
}
.tb-legend-keys {
td.tb-legend-label, td.tb-legend-value {
white-space: nowrap;
padding: 2px 10px;
}
.tb-legend-line {
width: 15px;
height: 3px;
display: inline-block;
vertical-align: middle;
}
.tb-legend-label {
text-align: left;
outline: none;
&.tb-horizontal {
width: 95%;
}
&.tb-hidden-label {
text-decoration: line-through;
opacity: 0.6;
}
}
}
}

42
ui/src/app/components/legend.tpl.html

@ -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.
-->
<table class="tb-legend">
<thead>
<tr class="tb-legend-header">
<th colspan="2"></th>
<th ng-if="legendConfig.showMin === true">{{ 'legend.min' | translate }}</th>
<th ng-if="legendConfig.showMax === true">{{ 'legend.max' | translate }}</th>
<th ng-if="legendConfig.showAvg === true">{{ 'legend.avg' | translate }}</th>
<th ng-if="legendConfig.showTotal === true">{{ 'legend.total' | translate }}</th>
</tr>
</thead>
<tbody>
<tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys">
<td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.color}"></span></td>
<td class="tb-legend-label"
ng-click="toggleHideData(legendKey.dataIndex)"
ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }">
{{ legendKey.label }}
</td>
<td class="tb-legend-value" ng-if="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td>
<td class="tb-legend-value" ng-if="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td>
<td class="tb-legend-value" ng-if="legendConfig.showAvg === true">{{ legendData.data[legendKey.dataIndex].avg }}</td>
<td class="tb-legend-value" ng-if="legendConfig.showTotal === true">{{ legendData.data[legendKey.dataIndex].total }}</td>
</tr>
</tbody>
</table>

2
ui/src/app/components/react/json-form-checkbox.jsx

@ -27,7 +27,7 @@ class ThingsboardCheckbox extends React.Component {
label={this.props.form.title}
disabled={this.props.form.readonly}
onCheck={(e, checked) => {this.props.onChangeValidate(e)}}
style={{paddingTop: '20px'}}
style={{paddingTop: '14px'}}
/>
);
}

131
ui/src/app/components/timeinterval.directive.js

@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.timeinterval', [])
.name;
/*@ngInject*/
function Timeinterval($compile, $templateCache, $translate) {
function Timeinterval($compile, $templateCache, timeService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
@ -39,62 +39,33 @@ function Timeinterval($compile, $templateCache, $translate) {
scope.mins = 1;
scope.secs = 0;
scope.predefIntervals = [
{
name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'),
value: 10 * 1000
},
{
name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'),
value: 30 * 1000
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'),
value: 60 * 1000
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'),
value: 2 * 60 * 1000
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'),
value: 5 * 60 * 1000
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'),
value: 10 * 60 * 1000
},
{
name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'),
value: 30 * 60 * 1000
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'),
value: 60 * 60 * 1000
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'),
value: 2 * 60 * 60 * 1000
},
{
name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'),
value: 10 * 60 * 60 * 1000
},
{
name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'),
value: 24 * 60 * 60 * 1000
},
{
name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'),
value: 7 * 24 * 60 * 60 * 1000
},
{
name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'),
value: 30 * 24 * 60 * 60 * 1000
}
];
scope.advanced = false;
scope.boundInterval = function() {
var min = timeService.boundMinInterval(scope.min);
var max = timeService.boundMaxInterval(scope.max);
scope.intervals = timeService.getIntervals(scope.min, scope.max);
if (scope.rendered) {
var newIntervalMs = ngModelCtrl.$viewValue;
if (newIntervalMs < min) {
newIntervalMs = min;
} else if (newIntervalMs > max) {
newIntervalMs = max;
}
if (!scope.advanced) {
newIntervalMs = timeService.boundToPredefinedInterval(min, max, newIntervalMs);
}
if (newIntervalMs !== ngModelCtrl.$viewValue) {
scope.setIntervalMs(newIntervalMs);
scope.updateView();
}
}
}
scope.setIntervalMs = function (intervalMs) {
if (!scope.advanced) {
scope.intervalMs = intervalMs;
}
var intervalSeconds = Math.floor(intervalMs / 1000);
scope.days = Math.floor(intervalSeconds / 86400);
scope.hours = Math.floor((intervalSeconds % 86400) / 3600);
@ -105,6 +76,9 @@ function Timeinterval($compile, $templateCache, $translate) {
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var intervalMs = ngModelCtrl.$viewValue;
if (!scope.rendered) {
scope.advanced = !timeService.matchesExistingInterval(scope.min, scope.max, intervalMs);
}
scope.setIntervalMs(intervalMs);
}
scope.rendered = true;
@ -115,10 +89,15 @@ function Timeinterval($compile, $templateCache, $translate) {
return;
}
var value = null;
var intervalMs = (scope.days * 86400 +
var intervalMs;
if (!scope.advanced) {
intervalMs = scope.intervalMs;
} else {
intervalMs = (scope.days * 86400 +
scope.hours * 3600 +
scope.mins * 60 +
scope.secs) * 1000;
}
if (!isNaN(intervalMs) && intervalMs > 0) {
value = intervalMs;
ngModelCtrl.$setValidity('tb-timeinterval', true);
@ -126,6 +105,7 @@ function Timeinterval($compile, $templateCache, $translate) {
ngModelCtrl.$setValidity('tb-timeinterval', !scope.required);
}
ngModelCtrl.$setViewValue(value);
scope.boundInterval();
}
scope.$watch('required', function (newRequired, prevRequired) {
@ -134,6 +114,38 @@ function Timeinterval($compile, $templateCache, $translate) {
}
});
scope.$watch('min', function (newMin, prevMin) {
if (angular.isDefined(newMin) && newMin !== prevMin) {
scope.updateView();
}
});
scope.$watch('max', function (newMax, prevMax) {
if (angular.isDefined(newMax) && newMax !== prevMax) {
scope.updateView();
}
});
scope.$watch('intervalMs', function (newIntervalMs, prevIntervalMs) {
if (angular.isDefined(newIntervalMs) && newIntervalMs !== prevIntervalMs) {
scope.updateView();
}
});
scope.$watch('advanced', function (newAdvanced, prevAdvanced) {
if (angular.isDefined(newAdvanced) && newAdvanced !== prevAdvanced) {
if (!scope.advanced) {
scope.intervalMs = (scope.days * 86400 +
scope.hours * 3600 +
scope.mins * 60 +
scope.secs) * 1000;
} else {
scope.setIntervalMs(scope.intervalMs);
}
scope.updateView();
}
});
scope.$watch('secs', function (newSecs) {
if (angular.isUndefined(newSecs)) {
return;
@ -198,6 +210,8 @@ function Timeinterval($compile, $templateCache, $translate) {
scope.updateView();
});
scope.boundInterval();
$compile(element.contents())(scope);
}
@ -206,7 +220,10 @@ function Timeinterval($compile, $templateCache, $translate) {
restrict: "E",
require: "^ngModel",
scope: {
required: '=ngRequired'
required: '=ngRequired',
min: '=?',
max: '=?',
predefinedName: '=?'
},
link: linker
};

10
ui/src/app/components/timeinterval.scss

@ -14,6 +14,7 @@
* limitations under the License.
*/
tb-timeinterval {
min-width: 355px;
md-input-container {
margin-bottom: 0px;
.md-errors-spacer {
@ -25,10 +26,13 @@ tb-timeinterval {
width: 150px;
}
}
}
tb-timeinterval {
.md-input {
width: 70px !important;
}
.advanced-switch {
margin-top: 0;
}
.advanced-label {
margin: 5px 0;
}
}

66
ui/src/app/components/timeinterval.tpl.html

@ -15,33 +15,41 @@
limitations under the License.
-->
<section layout="row" layout-align="start start">
<md-input-container>
<label translate>timeinterval.days</label>
<input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}">
</md-input-container>
<md-input-container>
<label translate>timeinterval.hours</label>
<input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}">
</md-input-container>
<md-input-container>
<label translate>timeinterval.minutes</label>
<input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}">
</md-input-container>
<md-input-container>
<label translate>timeinterval.seconds</label>
<input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}">
</md-input-container>
<md-menu md-position-mode="target-right target">
<md-button class="md-icon-button" aria-label="Open intervals" ng-click="$mdOpenMenu($event)">
<md-icon md-menu-origin aria-label="arrow_drop_down" class="material-icons">arrow_drop_down</md-icon>
</md-button>
<md-menu-content width="4">
<md-menu-item ng-repeat="interval in predefIntervals" >
<md-button ng-click="setIntervalMs(interval.value)">
<span>{{interval.name}}</span>
</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
<section layout="row">
<section layout="column" flex ng-show="advanced">
<label class="tb-small" translate>{{ predefinedName }}</label>
<section layout="row" layout-align="start start" flex>
<md-input-container>
<label translate>timeinterval.days</label>
<input type="number" ng-model="days" step="1" aria-label="{{ 'timeinterval.days' | translate }}">
</md-input-container>
<md-input-container>
<label translate>timeinterval.hours</label>
<input type="number" ng-model="hours" step="1" aria-label="{{ 'timeinterval.hours' | translate }}">
</md-input-container>
<md-input-container>
<label translate>timeinterval.minutes</label>
<input type="number" ng-model="mins" step="1" aria-label="{{ 'timeinterval.minutes' | translate }}">
</md-input-container>
<md-input-container>
<label translate>timeinterval.seconds</label>
<input type="number" ng-model="secs" step="1" aria-label="{{ 'timeinterval.seconds' | translate }}">
</md-input-container>
</section>
</section>
<section layout="row" flex ng-show="!advanced">
<md-input-container flex>
<label translate>{{ predefinedName }}</label>
<md-select ng-model="intervalMs" style="min-width: 150px;" aria-label="predefined-interval">
<md-option ng-repeat="interval in intervals" ng-value="interval.value">
{{interval.name}}
</md-option>
</md-select>
</md-input-container>
</section>
<section layout="column" layout-align="center center">
<label class="tb-small advanced-label" translate>timeinterval.advanced</label>
<md-switch class="advanced-switch" ng-model="advanced" aria-label="predefined-switcher">
</md-switch>
</section>
</section>

4
ui/src/app/components/timewindow-button.tpl.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<md-button class="md-raised md-primary" ng-click="openEditMode($event)">
<md-icon class="material-icons">date_range</md-icon>
<md-button ng-disabled="disabled" class="md-raised md-primary" ng-click="openEditMode($event)">
<ng-md-icon icon="query_builder"></ng-md-icon>
<span>{{model.displayValue}}</span>
</md-button>

52
ui/src/app/components/timewindow-panel.controller.js

@ -14,14 +14,23 @@
* limitations under the License.
*/
/*@ngInject*/
export default function TimewindowPanelController(mdPanelRef, $scope, timewindow, historyOnly, onTimewindowUpdate) {
export default function TimewindowPanelController(mdPanelRef, $scope, timeService, types, timewindow, historyOnly, aggregation, onTimewindowUpdate) {
var vm = this;
vm._mdPanelRef = mdPanelRef;
vm.timewindow = timewindow;
vm.historyOnly = historyOnly;
vm.aggregation = aggregation;
vm.onTimewindowUpdate = onTimewindowUpdate;
vm.aggregationTypes = types.aggregation;
vm.showLimit = showLimit;
vm.showRealtimeAggInterval = showRealtimeAggInterval;
vm.showHistoryAggInterval = showHistoryAggInterval;
vm.minRealtimeAggInterval = minRealtimeAggInterval;
vm.maxRealtimeAggInterval = maxRealtimeAggInterval;
vm.minHistoryAggInterval = minHistoryAggInterval;
vm.maxHistoryAggInterval = maxHistoryAggInterval;
if (vm.historyOnly) {
vm.timewindow.selectedTab = 1;
@ -46,4 +55,45 @@ export default function TimewindowPanelController(mdPanelRef, $scope, timewindow
vm.onTimewindowUpdate && vm.onTimewindowUpdate(vm.timewindow);
});
};
function showLimit() {
return vm.timewindow.aggregation.type === vm.aggregationTypes.none.value;
}
function showRealtimeAggInterval() {
return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value &&
vm.timewindow.selectedTab === 0;
}
function showHistoryAggInterval() {
return vm.timewindow.aggregation.type !== vm.aggregationTypes.none.value &&
vm.timewindow.selectedTab === 1;
}
function minRealtimeAggInterval () {
return timeService.minIntervalLimit(vm.timewindow.realtime.timewindowMs);
}
function maxRealtimeAggInterval () {
return timeService.maxIntervalLimit(vm.timewindow.realtime.timewindowMs);
}
function minHistoryAggInterval () {
return timeService.minIntervalLimit(currentHistoryTimewindow());
}
function maxHistoryAggInterval () {
return timeService.maxIntervalLimit(currentHistoryTimewindow());
}
function currentHistoryTimewindow() {
if (vm.timewindow.history.historyType === 0) {
return vm.timewindow.history.timewindowMs;
} else {
return vm.timewindow.history.fixedTimewindow.endTimeMs -
vm.timewindow.history.fixedTimewindow.startTimeMs;
}
}
}

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

Loading…
Cancel
Save