Browse Source

Merge pull request #7288 from thingsboard/feature/latestTsAggregation

[3.4.2] Support of aggregation for the latest values in the widget data source
pull/7304/head
Andrew Shvayka 4 years ago
committed by GitHub
parent
commit
cfdcbd7780
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 247
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  2. 29
      application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java
  3. 25
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
  5. 29
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java
  6. 32
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java
  7. 29
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java
  8. 21
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java
  9. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  10. 37
      common/data/src/main/java/org/thingsboard/server/common/data/kv/AggTsKvEntry.java
  11. 3
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java
  12. 5
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
  13. 2
      common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
  14. 55
      common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQueryResult.java
  15. 8
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
  16. 26
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntryAggWrapper.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
  18. 29
      common/data/src/main/java/org/thingsboard/server/common/data/query/ComparisonTsValue.java
  19. 6
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java
  20. 2
      common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java
  21. 12
      common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java
  22. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  23. 20
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java
  24. 9
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/ts/TimescaleTsKvEntity.java
  25. 13
      dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java
  26. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java
  27. 154
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  28. 13
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java
  29. 3
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java
  30. 22
      dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java
  31. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  32. 98
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java
  33. 143
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  34. 37
      dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java
  35. 81
      dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
  36. 20
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  37. 65
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  38. 6
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  39. 4
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
  40. 28
      dao/src/test/java/org/thingsboard/server/dao/TimescaleDaoServiceTestSuite.java
  41. 65
      dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java
  42. 33
      dao/src/test/java/org/thingsboard/server/dao/service/DaoTimescaleTest.java
  43. 18
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  44. 23
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceTimescaleTest.java
  45. 28
      dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java
  46. 18
      dao/src/test/resources/timescale-test.properties
  47. 2
      pom.xml
  48. 187
      ui-ngx/src/app/core/api/data-aggregator.ts
  49. 564
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  50. 42
      ui-ngx/src/app/core/api/entity-data.service.ts
  51. 49
      ui-ngx/src/app/core/api/widget-subscription.ts
  52. 4
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  53. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  54. 1
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html
  55. 3
      ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts
  56. 69
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html
  57. 75
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss
  58. 137
      ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts
  59. 7
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  60. 7
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss
  61. 36
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  62. 3
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  63. 4
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  64. 49
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  65. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html
  66. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html
  67. 5
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  68. 39
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  69. 3
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  70. 34
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  71. 25
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss
  72. 1
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  73. 63
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  74. 53
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  75. 54
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  76. 8
      ui-ngx/src/app/shared/models/query/query.models.ts
  77. 127
      ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts
  78. 15
      ui-ngx/src/app/shared/models/time/time.models.ts
  79. 64
      ui-ngx/src/app/shared/models/widget.models.ts
  80. 17
      ui-ngx/src/assets/locale/locale.constant-en_US.json

247
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -22,7 +22,6 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
@ -30,13 +29,13 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.ComparisonTsValue;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
@ -51,6 +50,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.AggHistoryCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.telemetry.cmd.v2.AggTimeSeriesCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
@ -68,12 +70,12 @@ import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -83,6 +85,7 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j
@TbCoreComponent
@Service
@ -131,6 +134,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private int maxEntitiesPerAlarmSubscription;
@Value("${server.ws.dynamic_page_link.max_alarm_queries_per_refresh_interval:10}")
private int maxAlarmQueriesPerRefreshInterval;
@Value("${ui.dashboard.max_datapoints_limit:50000}")
private int maxDatapointLimit;
private ExecutorService wsCallBackExecutor;
private boolean tsInSqlDB;
@ -165,7 +170,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
if (ctx != null) {
log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd);
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null || cmd.getHistoryCmd() != null) {
if (cmd.hasAnyCmd()) {
ctx.clearEntitySubscriptions();
}
} else {
@ -173,6 +178,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
ctx = createSubCtx(session, cmd);
}
ctx.setCurrentCmd(cmd);
// Fetch entity list using entity data query
if (cmd.getQuery() != null) {
if (ctx.getQuery() == null) {
log.debug("[{}][{}] Initializing data using query: {}", session.getSessionId(), cmd.getCmdId(), cmd.getQuery());
@ -204,43 +211,143 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
finalCtx.setRefreshTask(task);
}
}
ListenableFuture<TbEntityDataSubCtx> historyFuture;
if (cmd.getHistoryCmd() != null) {
log.trace("[{}][{}] Going to process history command: {}", session.getSessionId(), cmd.getCmdId(), cmd.getHistoryCmd());
try {
historyFuture = handleHistoryCmd(ctx, cmd.getHistoryCmd());
} catch (RuntimeException e) {
handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd);
return;
try {
List<ListenableFuture<?>> cmdFutures = new ArrayList<>();
if (cmd.getAggHistoryCmd() != null) {
cmdFutures.add(handleAggHistoryCmd(ctx, cmd.getAggHistoryCmd()));
}
} else {
historyFuture = Futures.immediateFuture(ctx);
if (cmd.getAggTsCmd() != null) {
cmdFutures.add(handleAggTsCmd(ctx, cmd.getAggTsCmd()));
}
if (cmd.getHistoryCmd() != null) {
cmdFutures.add(handleHistoryCmd(ctx, cmd.getHistoryCmd()));
}
if (cmdFutures.isEmpty()) {
handleRegularCommands(ctx, cmd);
} else {
TbEntityDataSubCtx finalCtx = ctx;
Futures.addCallback(Futures.allAsList(cmdFutures), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<Object> result) {
handleRegularCommands(finalCtx, cmd);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process command", finalCtx.getSessionId(), finalCtx.getCmdId());
}
}, wsCallBackExecutor);
}
} catch (RuntimeException e) {
handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd);
}
}
private void handleRegularCommands(TbEntityDataSubCtx ctx, EntityDataCmd cmd) {
try {
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) {
if (cmd.getLatestCmd() != null) {
handleLatestCmd(ctx, cmd.getLatestCmd());
}
if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(ctx, cmd.getTsCmd());
}
} else {
checkAndSendInitialData(ctx);
}
} catch (RuntimeException e) {
handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd);
}
Futures.addCallback(historyFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable TbEntityDataSubCtx theCtx) {
}
private void checkAndSendInitialData(@Nullable TbEntityDataSubCtx theCtx) {
if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription());
theCtx.sendWsMsg(update);
theCtx.setInitialDataSent(true);
}
}
private ListenableFuture<TbEntityDataSubCtx> handleAggHistoryCmd(TbEntityDataSubCtx ctx, AggHistoryCmd cmd) {
ConcurrentMap<Integer, ReadTsKvQueryInfo> queries = new ConcurrentHashMap<>();
for (AggKey key : cmd.getKeys()) {
if (key.getPreviousValueOnly() == null || !key.getPreviousValueOnly()) {
var query = new BaseReadTsKvQuery(key.getKey(), cmd.getStartTs(), cmd.getEndTs(), cmd.getEndTs() - cmd.getStartTs(), 1, key.getAgg());
queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, false));
}
if (key.getPreviousStartTs() != null && key.getPreviousEndTs() != null && key.getPreviousEndTs() >= key.getPreviousStartTs()) {
var query = new BaseReadTsKvQuery(key.getKey(), key.getPreviousStartTs(), key.getPreviousEndTs(), key.getPreviousEndTs() - key.getPreviousStartTs(), 1, key.getAgg());
queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, true));
}
}
return handleAggCmd(ctx, cmd.getKeys(), queries, cmd.getStartTs(), cmd.getEndTs(), false);
}
private ListenableFuture<TbEntityDataSubCtx> handleAggTsCmd(TbEntityDataSubCtx ctx, AggTimeSeriesCmd cmd) {
ConcurrentMap<Integer, ReadTsKvQueryInfo> queries = new ConcurrentHashMap<>();
for (AggKey key : cmd.getKeys()) {
var query = new BaseReadTsKvQuery(key.getKey(), cmd.getStartTs(), cmd.getStartTs() + cmd.getTimeWindow(), cmd.getTimeWindow(), 1, key.getAgg());
queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, false));
}
return handleAggCmd(ctx, cmd.getKeys(), queries, cmd.getStartTs(), cmd.getStartTs() + cmd.getTimeWindow(), true);
}
private ListenableFuture<TbEntityDataSubCtx> handleAggCmd(TbEntityDataSubCtx ctx, List<AggKey> keys, ConcurrentMap<Integer, ReadTsKvQueryInfo> queries,
long startTs, long endTs, boolean subscribe) {
Map<EntityData, ListenableFuture<List<ReadTsKvQueryResult>>> fetchResultMap = new HashMap<>();
List<EntityData> entityDataList = ctx.getData().getData();
List<ReadTsKvQuery> queryList = queries.values().stream().map(ReadTsKvQueryInfo::getQuery).collect(Collectors.toList());
entityDataList.forEach(entityData -> fetchResultMap.put(entityData,
tsService.findAllByQueries(ctx.getTenantId(), entityData.getEntityId(), queryList)));
return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> {
// Map that holds last ts for each key for each entity.
Map<EntityData, Map<String, Long>> lastTsEntityMap = new HashMap<>();
fetchResultMap.forEach((entityData, future) -> {
try {
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) {
if (cmd.getLatestCmd() != null) {
handleLatestCmd(theCtx, cmd.getLatestCmd());
}
if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(theCtx, cmd.getTsCmd());
Map<String, Long> lastTsMap = new HashMap<>();
lastTsEntityMap.put(entityData, lastTsMap);
List<ReadTsKvQueryResult> queryResults = future.get();
if (queryResults != null) {
for (ReadTsKvQueryResult queryResult : queryResults) {
ReadTsKvQueryInfo queryInfo = queries.get(queryResult.getQueryId());
ComparisonTsValue comparisonTsValue = entityData.getAggLatest().computeIfAbsent(queryInfo.getKey().getId(), agg -> new ComparisonTsValue());
if (queryInfo.isPrevious()) {
comparisonTsValue.setPrevious(queryResult.toTsValue());
} else {
comparisonTsValue.setCurrent(queryResult.toTsValue());
lastTsMap.put(queryInfo.getQuery().getKey(), queryResult.getLastEntryTs());
}
}
} else if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription());
theCtx.sendWsMsg(update);
theCtx.setInitialDataSent(true);
}
} catch (RuntimeException e) {
handleWsCmdRuntimeException(theCtx.getSessionId(), e, cmd);
// Populate with empty values if no data found.
keys.forEach(key -> {
entityData.getAggLatest().putIfAbsent(key.getId(), new ComparisonTsValue(TsValue.EMPTY, TsValue.EMPTY));
});
} catch (InterruptedException | ExecutionException e) {
log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e);
ctx.sendWsMsg(new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to fetch historical data!"));
}
});
ctx.getWsLock().lock();
try {
EntityDataUpdate update;
if (!ctx.isInitialDataSent()) {
update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.setInitialDataSent(true);
} else {
update = new EntityDataUpdate(ctx.getCmdId(), null, entityDataList, ctx.getMaxEntitiesPerDataSubscription());
}
if (subscribe) {
ctx.createTimeSeriesSubscriptions(lastTsEntityMap, startTs, endTs, true);
}
ctx.sendWsMsg(update);
entityDataList.forEach(ed -> ed.getTimeseries().clear());
} finally {
ctx.getWsLock().unlock();
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process command", session.getSessionId(), cmd.getCmdId());
}
return ctx;
}, wsCallBackExecutor);
}
@ -416,36 +523,58 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
private ListenableFuture<TbEntityDataSubCtx> handleGetTsCmd(TbEntityDataSubCtx ctx, GetTsCmd cmd, boolean subscribe) {
Map<Integer, String> queriesKeys = new ConcurrentHashMap<>();
List<String> keys = cmd.getKeys();
List<ReadTsKvQuery> finalTsKvQueryList;
List<ReadTsKvQuery> tsKvQueryList = cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery(
key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg()
)).collect(Collectors.toList());
List<ReadTsKvQuery> tsKvQueryList = keys.stream().map(key -> {
var query = new BaseReadTsKvQuery(
key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg()
);
queriesKeys.put(query.getId(), query.getKey());
return query;
}).collect(Collectors.toList());
if (cmd.isFetchLatestPreviousPoint()) {
finalTsKvQueryList = new ArrayList<>(tsKvQueryList);
finalTsKvQueryList.addAll(cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery(
key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg()
)).collect(Collectors.toList()));
finalTsKvQueryList.addAll(keys.stream().map(key -> {
var query = new BaseReadTsKvQuery(
key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg());
queriesKeys.put(query.getId(), query.getKey());
return query;
}
).collect(Collectors.toList()));
} else {
finalTsKvQueryList = tsKvQueryList;
}
Map<EntityData, ListenableFuture<List<TsKvEntry>>> fetchResultMap = new HashMap<>();
ctx.getData().getData().forEach(entityData -> fetchResultMap.put(entityData,
tsService.findAll(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList)));
Map<EntityData, ListenableFuture<List<ReadTsKvQueryResult>>> fetchResultMap = new HashMap<>();
List<EntityData> entityDataList = ctx.getData().getData();
entityDataList.forEach(entityData -> fetchResultMap.put(entityData,
tsService.findAllByQueries(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList)));
return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> {
// Map that holds last ts for each key for each entity.
Map<EntityData, Map<String, Long>> lastTsEntityMap = new HashMap<>();
fetchResultMap.forEach((entityData, future) -> {
Map<String, List<TsValue>> keyData = new LinkedHashMap<>();
cmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>()));
try {
List<TsKvEntry> entityTsData = future.get();
if (entityTsData != null) {
entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString())));
Map<String, Long> lastTsMap = new HashMap<>();
lastTsEntityMap.put(entityData, lastTsMap);
List<ReadTsKvQueryResult> queryResults = future.get();
if (queryResults != null) {
for (ReadTsKvQueryResult queryResult : queryResults) {
String queryKey = queriesKeys.get(queryResult.getQueryId());
entityData.getTimeseries().put(queryKey, queryResult.toTsValues());
lastTsMap.put(queryKey, queryResult.getLastEntryTs());
}
}
keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()])));
// Populate with empty values if no data found.
keys.forEach(key -> {
if (!entityData.getTimeseries().containsKey(key)) {
entityData.getTimeseries().put(key, new TsValue[0]);
}
});
if (cmd.isFetchLatestPreviousPoint()) {
entityData.getTimeseries().values().forEach(dataArray -> {
Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs()));
});
entityData.getTimeseries().values().forEach(dataArray -> Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs())));
}
} catch (InterruptedException | ExecutionException e) {
log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e);
@ -459,13 +588,13 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.setInitialDataSent(true);
} else {
update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription());
update = new EntityDataUpdate(ctx.getCmdId(), null, entityDataList, ctx.getMaxEntitiesPerDataSubscription());
}
if (subscribe) {
ctx.createTimeseriesSubscriptions(keys.stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).collect(Collectors.toList()), cmd.getStartTs(), cmd.getEndTs());
ctx.createTimeSeriesSubscriptions(lastTsEntityMap, cmd.getStartTs(), cmd.getEndTs());
}
ctx.sendWsMsg(update);
ctx.getData().getData().forEach(ed -> ed.getTimeseries().clear());
entityDataList.forEach(ed -> ed.getTimeseries().clear());
} finally {
ctx.getWsLock().unlock();
}
@ -533,11 +662,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
ctx.getWsLock().lock();
try {
ctx.createLatestValuesSubscriptions(latestCmd.getKeys());
if (!ctx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.sendWsMsg(update);
ctx.setInitialDataSent(true);
}
checkAndSendInitialData(ctx);
} finally {
ctx.getWsLock().unlock();
}

29
application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2022 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.service.subscription;
import lombok.Data;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.service.telemetry.cmd.v2.AggKey;
@Data
public class ReadTsKvQueryInfo {
private final AggKey key;
private final ReadTsKvQuery query;
private final boolean previous;
}

25
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java

@ -86,7 +86,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
} else {
oldDataMap = Collections.emptyMap();
}
Map<EntityId, EntityData> newDataMap = newData.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a,b)-> a));
Map<EntityId, EntityData> newDataMap = newData.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a, b) -> a));
if (oldDataMap.size() == newDataMap.size() && oldDataMap.keySet().equals(newDataMap.keySet())) {
log.trace("[{}][{}] No updates to entity data found", sessionRef.getSessionId(), cmdId);
} else {
@ -122,8 +122,17 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
createSubscriptions(keys, true, 0, 0);
}
public void createTimeseriesSubscriptions(List<EntityKey> keys, long startTs, long endTs) {
createSubscriptions(keys, false, startTs, endTs);
public void createTimeSeriesSubscriptions(Map<EntityData, Map<String, Long>> entityKeyStates, long startTs, long endTs) {
createTimeSeriesSubscriptions(entityKeyStates, startTs, endTs, false);
}
public void createTimeSeriesSubscriptions(Map<EntityData, Map<String, Long>> entityKeyStates, long startTs, long endTs, boolean resultToLatestValues) {
entityKeyStates.forEach((entityData, keyStates) -> {
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
subToEntityIdMap.put(subIdx, entityData.getEntityId());
localSubscriptionService.addSubscription(
createTsSub(entityData, subIdx, false, startTs, endTs, keyStates, resultToLatestValues));
});
}
private void createSubscriptions(List<EntityKey> keys, boolean latestValues, long startTs, long endTs) {
@ -191,6 +200,14 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
keyStates.put(k, ts);
});
}
return createTsSub(entityData, subIdx, latestValues, startTs, endTs, keyStates);
}
private TbTimeseriesSubscription createTsSub(EntityData entityData, int subIdx, boolean latestValues, long startTs, long endTs, Map<String, Long> keyStates) {
return createTsSub(entityData, subIdx, latestValues, startTs, endTs, keyStates, latestValues);
}
private TbTimeseriesSubscription createTsSub(EntityData entityData, int subIdx, boolean latestValues, long startTs, long endTs, Map<String, Long> keyStates, boolean resultToLatestValues) {
log.trace("[{}][{}][{}] Creating time-series subscription for [{}] with keys: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), keyStates);
return TbTimeseriesSubscription.builder()
.serviceId(serviceId)
@ -198,7 +215,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityData.getEntityId())
.updateConsumer((sessionId, subscriptionUpdate) -> sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, latestValues))
.updateConsumer((sessionId, subscriptionUpdate) -> sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues))
.allKeys(false)
.keyStates(keyStates)
.latestValues(latestValues)

2
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java

@ -716,7 +716,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
"Cmd id is negative value!");
sendWsMsg(sessionRef, update);
return false;
} else if (cmd.getQuery() == null && cmd.getLatestCmd() == null && cmd.getHistoryCmd() == null && cmd.getTsCmd() == null) {
} else if (cmd.getQuery() == null && !cmd.hasAnyCmd()) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Query is empty!");
sendWsMsg(sessionRef, update);

29
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2022 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.service.telemetry.cmd.v2;
import lombok.Data;
import java.util.List;
@Data
public class AggHistoryCmd {
private List<AggKey> keys;
private long startTs;
private long endTs;
}

32
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2022 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.service.telemetry.cmd.v2;
import lombok.Data;
import org.thingsboard.server.common.data.kv.Aggregation;
@Data
public class AggKey {
private int id;
private String key;
private Aggregation agg;
private Long previousStartTs;
private Long previousEndTs;
private Boolean previousValueOnly;
}

29
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2022 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.service.telemetry.cmd.v2;
import lombok.Data;
import java.util.List;
@Data
public class AggTimeSeriesCmd {
private List<AggKey> keys;
private long startTs;
private long timeWindow;
}

21
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -30,17 +31,35 @@ public class EntityDataCmd extends DataCmd {
private final LatestValueCmd latestCmd;
@Getter
private final TimeSeriesCmd tsCmd;
@Getter
private final AggHistoryCmd aggHistoryCmd;
@Getter
private final AggTimeSeriesCmd aggTsCmd;
public EntityDataCmd(int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd, LatestValueCmd latestCmd, TimeSeriesCmd tsCmd) {
this(cmdId, query, historyCmd, latestCmd, tsCmd, null, null);
}
@JsonCreator
public EntityDataCmd(@JsonProperty("cmdId") int cmdId,
@JsonProperty("query") EntityDataQuery query,
@JsonProperty("historyCmd") EntityHistoryCmd historyCmd,
@JsonProperty("latestCmd") LatestValueCmd latestCmd,
@JsonProperty("tsCmd") TimeSeriesCmd tsCmd) {
@JsonProperty("tsCmd") TimeSeriesCmd tsCmd,
@JsonProperty("aggHistoryCmd") AggHistoryCmd aggHistoryCmd,
@JsonProperty("aggTsCmd") AggTimeSeriesCmd aggTsCmd) {
super(cmdId);
this.query = query;
this.historyCmd = historyCmd;
this.latestCmd = latestCmd;
this.tsCmd = tsCmd;
this.aggHistoryCmd = aggHistoryCmd;
this.aggTsCmd = aggTsCmd;
}
@JsonIgnore
public boolean hasAnyCmd() {
return historyCmd != null || latestCmd != null || tsCmd != null || aggHistoryCmd != null || aggTsCmd != null;
}
}

3
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -32,6 +33,8 @@ import java.util.List;
*/
public interface TimeseriesService {
ListenableFuture<List<ReadTsKvQueryResult>> findAllByQueries(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<List<TsKvEntry>> findAll(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys);

37
common/data/src/main/java/org/thingsboard/server/common/data/kv/AggTsKvEntry.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2022 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;
import lombok.ToString;
import org.thingsboard.server.common.data.query.TsValue;
@ToString
public class AggTsKvEntry extends BasicTsKvEntry {
private static final long serialVersionUID = -1933884317450255935L;
private final long count;
public AggTsKvEntry(long ts, KvEntry kv, long count) {
super(ts, kv);
this.count = count;
}
@Override
public TsValue toTsValue() {
return new TsValue(ts, getValueAsString(), count);
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java

@ -31,8 +31,7 @@ public class BaseReadTsKvQuery extends BaseTsKvQuery implements ReadTsKvQuery {
this(key, startTs, endTs, interval, limit, aggregation, "DESC");
}
public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation,
String order) {
public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, String order) {
super(key, startTs, endTs);
this.interval = interval;
this.limit = limit;

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

@ -20,11 +20,16 @@ import lombok.Data;
@Data
public class BaseTsKvQuery implements TsKvQuery {
private static final ThreadLocal<Integer> idSeq = ThreadLocal.withInitial(() -> 0);
private final int id;
private final String key;
private final long startTs;
private final long endTs;
public BaseTsKvQuery(String key, long startTs, long endTs) {
this.id = idSeq.get();
idSeq.set(id + 1);
this.key = key;
this.startTs = startTs;
this.endTs = endTs;

2
common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java

@ -20,7 +20,7 @@ import java.util.Optional;
public class BasicTsKvEntry implements TsKvEntry {
private static final int MAX_CHARS_PER_DATA_POINT = 512;
private final long ts;
protected final long ts;
private final KvEntry kv;
public BasicTsKvEntry(long ts, KvEntry kv) {

55
common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQueryResult.java

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2022 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;
import lombok.Data;
import org.thingsboard.server.common.data.query.TsValue;
import java.util.ArrayList;
import java.util.List;
@Data
public class ReadTsKvQueryResult {
private final int queryId;
// Holds the data list;
private final List<TsKvEntry> data;
// Holds the max ts of the records that match aggregation intervals (not the ts of the aggregation window, but the ts of the last record among all the intervals)
private final long lastEntryTs;
public TsValue[] toTsValues() {
if (data != null && !data.isEmpty()) {
List<TsValue> queryValues = new ArrayList<>();
for (TsKvEntry v : data) {
queryValues.add(v.toTsValue()); // TODO: add count here.
}
return queryValues.toArray(new TsValue[queryValues.size()]);
} else {
return new TsValue[0];
}
}
public TsValue toTsValue() {
if (data == null || data.isEmpty()) {
return TsValue.EMPTY;
}
if (data.size() > 1) {
throw new RuntimeException("Query Result has multiple data points!");
}
return data.get(0).toTsValue();
}
}

8
common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java

@ -16,10 +16,11 @@
package org.thingsboard.server.common.data.kv;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.thingsboard.server.common.data.query.TsValue;
/**
* Represents time series KV data entry
*
*
* @author ashvayka
*
*/
@ -30,4 +31,9 @@ public interface TsKvEntry extends KvEntry {
@JsonIgnore
int getDataPoints();
@JsonIgnore
default TsValue toTsValue() {
return new TsValue(getTs(), getValueAsString());
}
}

26
common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntryAggWrapper.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2022 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;
import lombok.Data;
@Data
public class TsKvEntryAggWrapper {
private final TsKvEntry entry;
private final long lastEntryTs;
}

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

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.kv;
public interface TsKvQuery {
int getId();
String getKey();
long getStartTs();

29
common/data/src/main/java/org/thingsboard/server/common/data/query/ComparisonTsValue.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2022 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.query;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ComparisonTsValue {
private TsValue current;
private TsValue previous;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java

@ -16,15 +16,21 @@
package org.thingsboard.server.common.data.query;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map;
@Data
@RequiredArgsConstructor
public class EntityData {
private final EntityId entityId;
private final Map<EntityKeyType, Map<String, TsValue>> latest;
private final Map<String, TsValue[]> timeseries;
private final Map<Integer, ComparisonTsValue> aggLatest;
public EntityData(EntityId entityId, Map<EntityKeyType, Map<String, TsValue>> latest, Map<String, TsValue[]> timeseries) {
this(entityId, latest, timeseries, null);
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java

@ -23,6 +23,8 @@ import java.io.Serializable;
@ApiModel
@Data
public class EntityKey implements Serializable {
private static final long serialVersionUID = -6421575477523085543L;
private final EntityKeyType type;
private final String key;
}

12
common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java

@ -15,11 +15,23 @@
*/
package org.thingsboard.server.common.data.query;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TsValue {
public static final TsValue EMPTY = new TsValue(0, "");
private final long ts;
private final String value;
private final Long count;
public TsValue(long ts, String value) {
this(ts, value, null);
}
}

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

@ -631,7 +631,7 @@ public class ModelConstants {
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN)};
protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN), max(TS_COLUMN)};
protected 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), min(JSON_VALUE_COLUMN)});

20
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractTsKvEntity.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import org.thingsboard.server.common.data.kv.AggTsKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
@ -80,6 +81,18 @@ public abstract class AbstractTsKvEntity implements ToData<TsKvEntry> {
@Transient
protected String strKey;
@Transient
protected Long aggValuesLastTs;
@Transient
protected Long aggValuesCount;
public AbstractTsKvEntity() {
}
public AbstractTsKvEntity(Long aggValuesLastTs) {
this.aggValuesLastTs = aggValuesLastTs;
}
public abstract boolean isNotEmpty();
protected static boolean isAllNull(Object... args) {
@ -105,7 +118,12 @@ public abstract class AbstractTsKvEntity implements ToData<TsKvEntry> {
} else if (jsonValue != null) {
kvEntry = new JsonDataEntry(strKey, jsonValue);
}
return new BasicTsKvEntry(ts, kvEntry);
if (aggValuesCount == null) {
return new BasicTsKvEntry(ts, kvEntry);
} else {
return new AggTsKvEntry(ts, kvEntry, aggValuesCount);
}
}
}

9
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/ts/TimescaleTsKvEntity.java

@ -62,6 +62,7 @@ import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.F
@ColumnResult(name = "doubleCountValue", type = Long.class),
@ColumnResult(name = "strValue", type = String.class),
@ColumnResult(name = "aggType", type = String.class),
@ColumnResult(name = "maxAggTs", type = Long.class),
}
),
}),
@ -78,6 +79,7 @@ import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.F
@ColumnResult(name = "longValueCount", type = Long.class),
@ColumnResult(name = "doubleValueCount", type = Long.class),
@ColumnResult(name = "jsonValueCount", type = Long.class),
@ColumnResult(name = "maxAggTs", type = Long.class),
}
)
}),
@ -114,7 +116,8 @@ public final class TimescaleTsKvEntity extends AbstractTsKvEntity {
public TimescaleTsKvEntity() {
}
public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType) {
public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType, Long aggValuesLastTs) {
super(aggValuesLastTs);
if (!StringUtils.isEmpty(strValue)) {
this.strValue = strValue;
}
@ -135,6 +138,7 @@ public final class TimescaleTsKvEntity extends AbstractTsKvEntity {
} else {
this.doubleValue = 0.0;
}
this.aggValuesCount = totalCount;
break;
case SUM:
if (doubleCountValue > 0) {
@ -157,7 +161,8 @@ public final class TimescaleTsKvEntity extends AbstractTsKvEntity {
}
}
public TimescaleTsKvEntity(Long tsBucket, Long interval, Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount) {
public TimescaleTsKvEntity(Long tsBucket, Long interval, Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount, Long aggValuesLastTs) {
super(aggValuesLastTs);
if (!isAllNull(tsBucket, interval, booleanValueCount, strValueCount, longValueCount, doubleValueCount, jsonValueCount)) {
this.ts = tsBucket + interval / 2;
if (booleanValueCount != 0) {

13
dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java

@ -16,12 +16,15 @@
package org.thingsboard.server.dao.model.sqlts.ts;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import javax.persistence.Entity;
import javax.persistence.IdClass;
import javax.persistence.Table;
import javax.persistence.Transient;
@EqualsAndHashCode(callSuper = true)
@Data
@Entity
@Table(name = "ts_kv")
@ -31,11 +34,13 @@ public final class TsKvEntity extends AbstractTsKvEntity {
public TsKvEntity() {
}
public TsKvEntity(String strValue) {
public TsKvEntity(String strValue, Long aggValuesLastTs) {
super(aggValuesLastTs);
this.strValue = strValue;
}
public TsKvEntity(Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String aggType) {
public TsKvEntity(Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String aggType, Long aggValuesLastTs) {
super(aggValuesLastTs);
if (!isAllNull(longValue, doubleValue, longCountValue, doubleCountValue)) {
switch (aggType) {
case AVG:
@ -52,6 +57,7 @@ public final class TsKvEntity extends AbstractTsKvEntity {
} else {
this.doubleValue = 0.0;
}
this.aggValuesCount = totalCount;
break;
case SUM:
if (doubleCountValue > 0) {
@ -74,7 +80,8 @@ public final class TsKvEntity extends AbstractTsKvEntity {
}
}
public TsKvEntity(Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount) {
public TsKvEntity(Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount, Long aggValuesLastTs) {
super(aggValuesLastTs);
if (!isAllNull(booleanValueCount, strValueCount, longValueCount, doubleValueCount)) {
if (booleanValueCount != 0) {
this.longValue = booleanValueCount;

4
dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java

@ -54,8 +54,8 @@ public class EntityDataAdapter {
EntityType entityType = EntityType.valueOf((String) row.get("entity_type"));
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, id);
Map<EntityKeyType, Map<String, TsValue>> latest = new HashMap<>();
Map<String, TsValue[]> timeseries = new HashMap<>();
EntityData entityData = new EntityData(entityId, latest, timeseries);
//Maybe avoid empty hashmaps?
EntityData entityData = new EntityData(entityId, latest, new HashMap<>(), new HashMap<>());
for (EntityKeyMapping mapping : selectionMapping) {
if (!mapping.isIgnore()) {
EntityKey entityKey = mapping.getEntityKey();

154
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java

@ -17,8 +17,6 @@ package org.thingsboard.server.dao.sqlts;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
@ -26,9 +24,9 @@ import org.springframework.data.domain.Sort;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
@ -47,10 +45,9 @@ import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j
public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSqlTimeseriesDao implements TimeseriesDao {
@ -81,7 +78,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
Comparator.comparing((Function<TsKvEntity, UUID>) AbstractTsKvEntity::getEntityId)
.thenComparing(AbstractTsKvEntity::getKey)
.thenComparing(AbstractTsKvEntity::getTs)
);
);
}
@PreDestroy
@ -114,32 +111,32 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
public ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
return processFindAllAsync(tenantId, entityId, queries);
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(entityId, query);
return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
} else {
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
long endPeriod = query.getEndTs();
List<ListenableFuture<Optional<TsKvEntity>>> futures = new ArrayList<>();
long startPeriod = query.getStartTs();
long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs());
long step = query.getInterval();
while (startPeriod <= endPeriod) {
while (startPeriod < endPeriod) {
long startTs = startPeriod;
long endTs = Math.min(startPeriod + step, endPeriod + 1);
long endTs = Math.min(startPeriod + step, endPeriod);
long ts = startTs + (endTs - startTs) / 2;
ListenableFuture<Optional<TsKvEntry>> aggregateTsKvEntry = findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation());
ListenableFuture<Optional<TsKvEntity>> aggregateTsKvEntry = findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation());
futures.add(aggregateTsKvEntry);
startPeriod = endTs;
}
return getTskvEntriesFuture(Futures.allAsList(futures));
return getReadTsKvQueryResultFuture(query, Futures.allAsList(futures));
}
}
private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) {
private ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) {
Integer keyId = getOrSaveKeyId(query.getKey());
List<TsKvEntity> tsKvEntities = tsKvRepository.findAllWithLimit(
entityId.getId(),
@ -147,125 +144,52 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
query.getStartTs(),
query.getEndTs(),
PageRequest.of(0, query.getLimit(),
Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative())));
Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative())));
tsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(query.getKey()));
return Futures.immediateFuture(DaoUtil.convertDataList(tsKvEntities));
List<TsKvEntry> tsKvEntries = DaoUtil.convertDataList(tsKvEntities);
long lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs());
return new ReadTsKvQueryResult(query.getId(), tsKvEntries, lastTs);
}
ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) {
List<CompletableFuture<TsKvEntity>> entitiesFutures = new ArrayList<>();
switchAggregation(entityId, key, startTs, endTs, aggregation, entitiesFutures);
return Futures.transform(setFutures(entitiesFutures), entity -> {
ListenableFuture<Optional<TsKvEntity>> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) {
return service.submit(() -> {
TsKvEntity entity = switchAggregation(entityId, key, startTs, endTs, aggregation);
if (entity != null && entity.isNotEmpty()) {
entity.setEntityId(entityId.getId());
entity.setStrKey(key);
entity.setTs(ts);
return Optional.of(DaoUtil.getData(entity));
return Optional.of(entity);
} else {
return Optional.empty();
}
}, MoreExecutors.directExecutor());
});
}
protected void switchAggregation(EntityId entityId, String key, long startTs, long endTs, Aggregation aggregation, List<CompletableFuture<TsKvEntity>> entitiesFutures) {
protected TsKvEntity switchAggregation(EntityId entityId, String key, long startTs, long endTs, Aggregation aggregation) {
var keyId = getOrSaveKeyId(key);
switch (aggregation) {
case AVG:
findAvg(entityId, key, startTs, endTs, entitiesFutures);
break;
return tsKvRepository.findAvg(entityId.getId(), keyId, startTs, endTs);
case MAX:
findMax(entityId, key, startTs, endTs, entitiesFutures);
break;
var max = tsKvRepository.findNumericMax(entityId.getId(), keyId, startTs, endTs);
if (max.isNotEmpty()) {
return max;
} else {
return tsKvRepository.findStringMax(entityId.getId(), keyId, startTs, endTs);
}
case MIN:
findMin(entityId, key, startTs, endTs, entitiesFutures);
break;
var min = tsKvRepository.findNumericMin(entityId.getId(), keyId, startTs, endTs);
if (min.isNotEmpty()) {
return min;
} else {
return tsKvRepository.findStringMin(entityId.getId(), keyId, startTs, endTs);
}
case SUM:
findSum(entityId, key, startTs, endTs, entitiesFutures);
break;
return tsKvRepository.findSum(entityId.getId(), keyId, startTs, endTs);
case COUNT:
findCount(entityId, key, startTs, endTs, entitiesFutures);
break;
return tsKvRepository.findCount(entityId.getId(), keyId, startTs, endTs);
default:
throw new IllegalArgumentException("Not supported aggregation type: " + aggregation);
}
}
protected void findCount(EntityId entityId, String key, long startTs, long endTs, List<CompletableFuture<TsKvEntity>> entitiesFutures) {
Integer keyId = getOrSaveKeyId(key);
entitiesFutures.add(tsKvRepository.findCount(
entityId.getId(),
keyId,
startTs,
endTs));
}
protected void findSum(EntityId entityId, String key, long startTs, long endTs, List<CompletableFuture<TsKvEntity>> entitiesFutures) {
Integer keyId = getOrSaveKeyId(key);
entitiesFutures.add(tsKvRepository.findSum(
entityId.getId(),
keyId,
startTs,
endTs));
}
protected void findMin(EntityId entityId, String key, long startTs, long endTs, List<CompletableFuture<TsKvEntity>> entitiesFutures) {
Integer keyId = getOrSaveKeyId(key);
entitiesFutures.add(tsKvRepository.findStringMin(
entityId.getId(),
keyId,
startTs,
endTs));
entitiesFutures.add(tsKvRepository.findNumericMin(
entityId.getId(),
keyId,
startTs,
endTs));
}
protected void findMax(EntityId entityId, String key, long startTs, long endTs, List<CompletableFuture<TsKvEntity>> entitiesFutures) {
Integer keyId = getOrSaveKeyId(key);
entitiesFutures.add(tsKvRepository.findStringMax(
entityId.getId(),
keyId,
startTs,
endTs));
entitiesFutures.add(tsKvRepository.findNumericMax(
entityId.getId(),
keyId,
startTs,
endTs));
}
protected void findAvg(EntityId entityId, String key, long startTs, long endTs, List<CompletableFuture<TsKvEntity>> entitiesFutures) {
Integer keyId = getOrSaveKeyId(key);
entitiesFutures.add(tsKvRepository.findAvg(
entityId.getId(),
keyId,
startTs,
endTs));
}
protected SettableFuture<TsKvEntity> setFutures(List<CompletableFuture<TsKvEntity>> entitiesFutures) {
SettableFuture<TsKvEntity> listenableFuture = SettableFuture.create();
CompletableFuture<List<TsKvEntity>> entities =
CompletableFuture.allOf(entitiesFutures.toArray(new CompletableFuture[entitiesFutures.size()]))
.thenApply(v -> entitiesFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
entities.whenComplete((tsKvEntities, throwable) -> {
if (throwable != null) {
listenableFuture.setException(throwable);
} else {
TsKvEntity result = null;
for (TsKvEntity entity : tsKvEntities) {
if (entity.isNotEmpty()) {
result = entity;
break;
}
}
listenableFuture.set(result);
}
});
return listenableFuture;
}
}

13
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java

@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
@ -38,6 +39,7 @@ import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j
public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseriesDao implements AggregationTimeseriesDao {
@ -86,22 +88,19 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries
}
}
protected ListenableFuture<List<TsKvEntry>> processFindAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries
protected ListenableFuture<List<ReadTsKvQueryResult>> processFindAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
List<ListenableFuture<ReadTsKvQueryResult>> futures = queries
.stream()
.map(query -> findAllAsync(tenantId, entityId, query))
.collect(Collectors.toList());
return Futures.transform(Futures.allAsList(futures), new Function<>() {
@Nullable
@Override
public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) {
public List<ReadTsKvQueryResult> apply(@Nullable List<ReadTsKvQueryResult> results) {
if (results == null || results.isEmpty()) {
return null;
}
return results.stream()
.filter(Objects::nonNull)
.flatMap(List::stream)
.collect(Collectors.toList());
return results.stream().filter(Objects::nonNull).collect(Collectors.toList());
}
}, service);
}

3
dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java

@ -19,11 +19,12 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.List;
public interface AggregationTimeseriesDao {
ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query);
ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query);
}

22
dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java

@ -22,14 +22,20 @@ import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionary;
import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionaryCompositeKey;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService;
import org.thingsboard.server.dao.sqlts.dictionary.TsKvDictionaryRepository;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -80,18 +86,20 @@ public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeni
return keyId;
}
protected ListenableFuture<List<TsKvEntry>> getTskvEntriesFuture(ListenableFuture<List<Optional<TsKvEntry>>> future) {
return Futures.transform(future, new Function<List<Optional<TsKvEntry>>, List<TsKvEntry>>() {
protected ListenableFuture<ReadTsKvQueryResult> getReadTsKvQueryResultFuture(ReadTsKvQuery query, ListenableFuture<List<Optional<? extends AbstractTsKvEntity>>> future) {
return Futures.transform(future, new Function<>() {
@Nullable
@Override
public List<TsKvEntry> apply(@Nullable List<Optional<TsKvEntry>> results) {
public ReadTsKvQueryResult apply(@Nullable List<Optional<? extends AbstractTsKvEntity>> results) {
if (results == null || results.isEmpty()) {
return null;
}
return results.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
List<? extends AbstractTsKvEntity> data = results.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
var lastTs = data.stream().map(AbstractTsKvEntity::getAggValuesLastTs).filter(Objects::nonNull).max(Long::compare);
if (lastTs.isEmpty()) {
lastTs = data.stream().map(AbstractTsKvEntity::getTs).filter(Objects::nonNull).max(Long::compare);
}
return new ReadTsKvQueryResult(query.getId(), DaoUtil.convertDataList(data), lastTs.orElse(query.getStartTs()));
}
}, service);
}

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
@ -190,7 +191,8 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
long endTs = query.getStartTs() - 1;
ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1,
Aggregation.NONE, DESC_ORDER);
return aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery);
return Futures.transform(aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery),
ReadTsKvQueryResult::getData, MoreExecutors.directExecutor());
}
protected ListenableFuture<TsKvEntry> getFindLatestFuture(EntityId entityId, String key) {

98
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java

@ -36,54 +36,79 @@ public class AggregationRepository {
public static final String FIND_SUM = "findSum";
public static final String FIND_COUNT = "findCount";
public static final String FROM_WHERE_CLAUSE = "FROM ts_kv tskv WHERE tskv.entity_id = cast(:entityId AS uuid) AND tskv.key= cast(:entityKey AS int) AND tskv.ts > :startTs AND tskv.ts <= :endTs GROUP BY tskv.entity_id, tskv.key, tsBucket ORDER BY tskv.entity_id, tskv.key, tsBucket";
public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType ";
public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType ";
public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType ";
public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType ";
public static final String FIND_COUNT_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount ";
public static final String FROM_WHERE_CLAUSE = "FROM ts_kv tskv WHERE " +
"tskv.entity_id = cast(:entityId AS uuid) " +
"AND tskv.key= cast(:entityKey AS int) " +
"AND tskv.ts >= :startTs AND tskv.ts < :endTs " +
"GROUP BY tskv.entity_id, tskv.key, tsBucket " +
"ORDER BY tskv.entity_id, tskv.key, tsBucket";
public static final String FIND_AVG_QUERY = "SELECT " +
"time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " +
"SUM(COALESCE(tskv.long_v, 0)) AS longValue, " +
"SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, " +
"SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " +
"SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " +
"null AS strValue, 'AVG' AS aggType, MAX(tskv.ts) AS maxAggTs ";
public static final String FIND_MAX_QUERY = "SELECT " +
"time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " +
"MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, " +
"MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, " +
"SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " +
"SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " +
"MAX(tskv.str_v) AS strValue, 'MAX' AS aggType, MAX(tskv.ts) AS maxAggTs ";
public static final String FIND_MIN_QUERY = "SELECT " +
"time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " +
"MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, " +
"MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, " +
"SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " +
"SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " +
"MIN(tskv.str_v) AS strValue, 'MIN' AS aggType, MAX(tskv.ts) AS maxAggTs ";
public static final String FIND_SUM_QUERY = "SELECT " +
"time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " +
"SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, " +
"SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " +
"SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " +
"null AS strValue, null AS jsonValue, 'SUM' AS aggType, MAX(tskv.ts) AS maxAggTs ";
public static final String FIND_COUNT_QUERY = "SELECT " +
"time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " +
"SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, " +
"SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, " +
"SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, " +
"SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, " +
"SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount, " +
"MAX(tskv.ts) AS maxAggTs ";
@PersistenceContext
private EntityManager entityManager;
@Async
public CompletableFuture<List<TimescaleTsKvEntity>> findAvg(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked")
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_AVG);
return CompletableFuture.supplyAsync(() -> resultList);
@SuppressWarnings("unchecked")
public List<TimescaleTsKvEntity> findAvg(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_AVG);
}
@Async
public CompletableFuture<List<TimescaleTsKvEntity>> findMax(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked")
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MAX);
return CompletableFuture.supplyAsync(() -> resultList);
@SuppressWarnings("unchecked")
public List<TimescaleTsKvEntity> findMax(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MAX);
}
@Async
public CompletableFuture<List<TimescaleTsKvEntity>> findMin(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked")
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MIN);
return CompletableFuture.supplyAsync(() -> resultList);
@SuppressWarnings("unchecked")
public List<TimescaleTsKvEntity> findMin(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MIN);
}
@Async
public CompletableFuture<List<TimescaleTsKvEntity>> findSum(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked")
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_SUM);
return CompletableFuture.supplyAsync(() -> resultList);
@SuppressWarnings("unchecked")
public List<TimescaleTsKvEntity> findSum(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_SUM);
}
@Async
public CompletableFuture<List<TimescaleTsKvEntity>> findCount(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked")
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_COUNT);
return CompletableFuture.supplyAsync(() -> resultList);
@SuppressWarnings("unchecked")
public List<TimescaleTsKvEntity> findCount(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_COUNT);
}
private List getResultList(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs, String query) {
@ -96,5 +121,4 @@ public class AggregationRepository {
.getResultList();
}
}

143
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java

@ -30,11 +30,13 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
@ -101,13 +103,13 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
public ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
return processFindAllAsync(tenantId, entityId, queries);
}
@Override
public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) {
int dataPointDays = getDataPointDays(tsKvEntry, computeTtl(ttl));
int dataPointDays = getDataPointDays(tsKvEntry, computeTtl(ttl));
String strKey = tsKvEntry.getKey();
Integer keyId = getOrSaveKeyId(strKey);
TimescaleTsKvEntity entity = new TimescaleTsKvEntity();
@ -148,15 +150,15 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(entityId, query);
return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
} else {
long startTs = query.getStartTs();
long endTs = query.getEndTs();
long endTs = Math.max(query.getStartTs() + 1, query.getEndTs());
long timeBucket = query.getInterval();
ListenableFuture<List<Optional<TsKvEntry>>> future = findAllAndAggregateAsync(entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation());
return getTskvEntriesFuture(future);
List<Optional<? extends AbstractTsKvEntity>> data = findAllAndAggregateAsync(entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation());
return getReadTsKvQueryResultFuture(query, Futures.immediateFuture(data));
}
}
@ -165,7 +167,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
super.cleanup(systemTtl);
}
private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) {
private ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) {
String strKey = query.getKey();
Integer keyId = getOrSaveKeyId(strKey);
List<TimescaleTsKvEntity> timescaleTsKvEntities = tsKvRepository.findAllWithLimit(
@ -174,105 +176,60 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
query.getStartTs(),
query.getEndTs(),
PageRequest.of(0, query.getLimit(),
Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative())));;
Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative())));
;
timescaleTsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(strKey));
return Futures.immediateFuture(DaoUtil.convertDataList(timescaleTsKvEntities));
var tsKvEntries = DaoUtil.convertDataList(timescaleTsKvEntities);
long lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs());
return new ReadTsKvQueryResult(query.getId(), tsKvEntries, lastTs);
}
private ListenableFuture<List<Optional<TsKvEntry>>> findAllAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long timeBucket, Aggregation aggregation) {
CompletableFuture<List<TimescaleTsKvEntity>> listCompletableFuture = switchAggregation(key, startTs, endTs, timeBucket, aggregation, entityId.getId());
SettableFuture<List<TimescaleTsKvEntity>> listenableFuture = SettableFuture.create();
listCompletableFuture.whenComplete((timescaleTsKvEntities, throwable) -> {
if (throwable != null) {
listenableFuture.setException(throwable);
} else {
listenableFuture.set(timescaleTsKvEntities);
}
});
return Futures.transform(listenableFuture, timescaleTsKvEntities -> {
if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) {
List<Optional<TsKvEntry>> result = new ArrayList<>();
timescaleTsKvEntities.forEach(entity -> {
if (entity != null && entity.isNotEmpty()) {
entity.setEntityId(entityId.getId());
entity.setStrKey(key);
result.add(Optional.of(DaoUtil.getData(entity)));
} else {
result.add(Optional.empty());
}
});
return result;
} else {
return Collections.emptyList();
}
}, MoreExecutors.directExecutor());
private List<Optional<? extends AbstractTsKvEntity>> findAllAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long timeBucket, Aggregation aggregation) {
long interval = endTs - startTs;
long remainingPart = interval % timeBucket;
List<TimescaleTsKvEntity> timescaleTsKvEntities;
if (remainingPart == 0) {
timescaleTsKvEntities = switchAggregation(key, startTs, endTs, timeBucket, aggregation, entityId.getId());
} else {
interval = interval - remainingPart;
timescaleTsKvEntities = new ArrayList<>();
timescaleTsKvEntities.addAll(switchAggregation(key, startTs, startTs + interval, timeBucket, aggregation, entityId.getId()));
timescaleTsKvEntities.addAll(switchAggregation(key, startTs + interval, endTs, remainingPart, aggregation, entityId.getId()));
}
if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) {
List<Optional<? extends AbstractTsKvEntity>> result = new ArrayList<>();
timescaleTsKvEntities.forEach(entity -> {
if (entity != null && entity.isNotEmpty()) {
entity.setEntityId(entityId.getId());
entity.setStrKey(key);
result.add(Optional.of(entity));
} else {
result.add(Optional.empty());
}
});
return result;
} else {
return Collections.emptyList();
}
}
private CompletableFuture<List<TimescaleTsKvEntity>> switchAggregation(String key, long startTs, long endTs, long timeBucket, Aggregation aggregation, UUID entityId) {
private List<TimescaleTsKvEntity> switchAggregation(String key, long startTs, long endTs, long timeBucket, Aggregation aggregation, UUID entityId) {
Integer keyId = getOrSaveKeyId(key);
switch (aggregation) {
case AVG:
return findAvg(key, startTs, endTs, timeBucket, entityId);
return aggregationRepository.findAvg(entityId, keyId, timeBucket, startTs, endTs);
case MAX:
return findMax(key, startTs, endTs, timeBucket, entityId);
return aggregationRepository.findMax(entityId, keyId, timeBucket, startTs, endTs);
case MIN:
return findMin(key, startTs, endTs, timeBucket, entityId);
return aggregationRepository.findMin(entityId, keyId, timeBucket, startTs, endTs);
case SUM:
return findSum(key, startTs, endTs, timeBucket, entityId);
return aggregationRepository.findSum(entityId, keyId, timeBucket, startTs, endTs);
case COUNT:
return findCount(key, startTs, endTs, timeBucket, entityId);
return aggregationRepository.findCount(entityId, keyId, timeBucket, startTs, endTs);
default:
throw new IllegalArgumentException("Not supported aggregation type: " + aggregation);
}
}
private CompletableFuture<List<TimescaleTsKvEntity>> findCount(String key, long startTs, long endTs, long timeBucket, UUID entityId) {
Integer keyId = getOrSaveKeyId(key);
return aggregationRepository.findCount(
entityId,
keyId,
timeBucket,
startTs,
endTs);
}
private CompletableFuture<List<TimescaleTsKvEntity>> findSum(String key, long startTs, long endTs, long timeBucket, UUID entityId) {
Integer keyId = getOrSaveKeyId(key);
return aggregationRepository.findSum(
entityId,
keyId,
timeBucket,
startTs,
endTs);
}
private CompletableFuture<List<TimescaleTsKvEntity>> findMin(String key, long startTs, long endTs, long timeBucket, UUID entityId) {
Integer keyId = getOrSaveKeyId(key);
return aggregationRepository.findMin(
entityId,
keyId,
timeBucket,
startTs,
endTs);
}
private CompletableFuture<List<TimescaleTsKvEntity>> findMax(String key, long startTs, long endTs, long timeBucket, UUID entityId) {
Integer keyId = getOrSaveKeyId(key);
return aggregationRepository.findMax(
entityId,
keyId,
timeBucket,
startTs,
endTs);
}
private CompletableFuture<List<TimescaleTsKvEntity>> findAvg(String key, long startTs, long endTs, long timeBucket, UUID entityId) {
Integer keyId = getOrSaveKeyId(key);
return aggregationRepository.findAvg(
entityId,
keyId,
timeBucket,
startTs,
endTs);
}
}

37
dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java

@ -20,14 +20,12 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvCompositeKey;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public interface TsKvRepository extends JpaRepository<TsKvEntity, TsKvCompositeKey> {
@ -48,82 +46,75 @@ public interface TsKvRepository extends JpaRepository<TsKvEntity, TsKvCompositeK
@Param("startTs") long startTs,
@Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(MAX(tskv.strValue)) FROM TsKvEntity tskv " +
@Query("SELECT new TsKvEntity(MAX(tskv.strValue), MAX(tskv.ts)) FROM TsKvEntity tskv " +
"WHERE tskv.strValue IS NOT NULL " +
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findStringMax(@Param("entityId") UUID entityId,
TsKvEntity findStringMax(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(MAX(COALESCE(tskv.longValue, -9223372036854775807)), " +
"MAX(COALESCE(tskv.doubleValue, -1.79769E+308)), " +
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'MAX') FROM TsKvEntity tskv " +
"'MAX', MAX(tskv.ts)) FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findNumericMax(@Param("entityId") UUID entityId,
TsKvEntity findNumericMax(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(MIN(tskv.strValue)) FROM TsKvEntity tskv " +
@Query("SELECT new TsKvEntity(MIN(tskv.strValue), MAX(tskv.ts)) FROM TsKvEntity tskv " +
"WHERE tskv.strValue IS NOT NULL " +
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findStringMin(@Param("entityId") UUID entityId,
TsKvEntity findStringMin(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(MIN(COALESCE(tskv.longValue, 9223372036854775807)), " +
"MIN(COALESCE(tskv.doubleValue, 1.79769E+308)), " +
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'MIN') FROM TsKvEntity tskv " +
"'MIN', MAX(tskv.ts)) FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findNumericMin(
TsKvEntity findNumericMin(
@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(SUM(CASE WHEN tskv.booleanValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.strValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.jsonValue IS NULL THEN 0 ELSE 1 END)) FROM TsKvEntity tskv " +
"SUM(CASE WHEN tskv.jsonValue IS NULL THEN 0 ELSE 1 END), MAX(tskv.ts)) FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findCount(@Param("entityId") UUID entityId,
TsKvEntity findCount(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " +
"SUM(COALESCE(tskv.doubleValue, 0.0)), " +
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'AVG') FROM TsKvEntity tskv " +
"'AVG', MAX(tskv.ts)) FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findAvg(@Param("entityId") UUID entityId,
TsKvEntity findAvg(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " +
"SUM(COALESCE(tskv.doubleValue, 0.0)), " +
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'SUM') FROM TsKvEntity tskv " +
"'SUM', MAX(tskv.ts)) FROM TsKvEntity tskv " +
"WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs")
CompletableFuture<TsKvEntity> findSum(@Param("entityId") UUID entityId,
TsKvEntity findSum(@Param("entityId") UUID entityId,
@Param("entityKey") int entityKey,
@Param("startTs") long startTs,
@Param("endTs") long endTs);

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

@ -19,6 +19,7 @@ import com.datastax.oss.driver.api.core.cql.Row;
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.kv.AggTsKvEntry;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.dao.nosql.TbResultSet;
import javax.annotation.Nullable;
@ -40,18 +42,20 @@ import java.util.stream.Collectors;
* Created by ashvayka on 20.02.17.
*/
@Slf4j
public class AggregatePartitionsFunction implements com.google.common.util.concurrent.AsyncFunction<List<TbResultSet>, Optional<TsKvEntry>> {
public class AggregatePartitionsFunction implements com.google.common.util.concurrent.AsyncFunction<List<TbResultSet>, Optional<TsKvEntryAggWrapper>> {
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 JSON_CNT_POS = 4;
private static final int LONG_POS = 5;
private static final int DOUBLE_POS = 6;
private static final int BOOL_POS = 7;
private static final int STR_POS = 8;
private static final int JSON_POS = 9;
private static final int MAX_TS_POS = 5;
private static final int LONG_POS = 6;
private static final int DOUBLE_POS = 7;
private static final int BOOL_POS = 8;
private static final int STR_POS = 9;
private static final int JSON_POS = 10;
private final Aggregation aggregation;
private final String key;
@ -66,29 +70,29 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
}
@Override
public ListenableFuture<Optional<TsKvEntry>> apply(@Nullable List<TbResultSet> rsList) {
log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation);
if (rsList == null || rsList.isEmpty()) {
return Futures.immediateFuture(Optional.empty());
}
return Futures.transform(
Futures.allAsList(
rsList.stream().map(rs -> rs.allRows(this.executor))
.collect(Collectors.toList())),
rowsList -> {
try {
AggregationResult aggResult = new AggregationResult();
for (List<Row> rs : rowsList) {
for (Row row : rs) {
processResultSetRow(row, aggResult);
public ListenableFuture<Optional<TsKvEntryAggWrapper>> apply(@Nullable List<TbResultSet> rsList) {
log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation);
if (rsList == null || rsList.isEmpty()) {
return Futures.immediateFuture(Optional.empty());
}
return Futures.transform(
Futures.allAsList(
rsList.stream().map(rs -> rs.allRows(this.executor))
.collect(Collectors.toList())),
rowsList -> {
try {
AggregationResult aggResult = new AggregationResult();
for (List<Row> rs : rowsList) {
for (Row row : rs) {
processResultSetRow(row, aggResult);
}
}
return processAggregationResult(aggResult);
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e);
return Optional.empty();
}
}
return processAggregationResult(aggResult);
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e);
return Optional.empty();
}
}, this.executor);
}, this.executor);
}
private void processResultSetRow(Row row, AggregationResult aggResult) {
@ -105,6 +109,7 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
long boolCount = row.getLong(BOOL_CNT_POS);
long strCount = row.getLong(STR_CNT_POS);
long jsonCount = row.getLong(JSON_CNT_POS);
long aggValuesLastTs = row.getLong(MAX_TS_POS);
if (longCount > 0 || doubleCount > 0) {
if (longCount > 0) {
@ -134,6 +139,8 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
return;
}
aggResult.aggValuesLastTs = Math.max(aggResult.aggValuesLastTs, aggValuesLastTs);
if (aggregation == Aggregation.COUNT) {
aggResult.count += curCount;
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) {
@ -231,34 +238,37 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
}
}
private Optional<TsKvEntry> processAggregationResult(AggregationResult aggResult) {
private Optional<TsKvEntryAggWrapper> processAggregationResult(AggregationResult aggResult) {
Optional<TsKvEntry> result;
if (aggResult.dataType == null) {
result = Optional.empty();
} else if (aggregation == Aggregation.COUNT) {
result = Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggResult.count)));
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) {
result = processAvgOrSumResult(aggResult);
result = processAvgOrSumResult(aggregation, aggResult);
} else if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) {
result = processMinOrMaxResult(aggResult);
} else {
result = Optional.empty();
}
if (!result.isPresent()) {
if (result.isEmpty()) {
log.trace("[{}][{}][{}] Aggregated data is empty.", key, ts, aggregation);
}
return result;
return result.map(tsKvEntry -> new TsKvEntryAggWrapper(tsKvEntry, aggResult.aggValuesLastTs));
}
private Optional<TsKvEntry> processAvgOrSumResult(AggregationResult aggResult) {
private Optional<TsKvEntry> processAvgOrSumResult(Aggregation aggregation, AggregationResult aggResult) {
if (aggResult.count == 0 || (aggResult.dataType == DataType.DOUBLE && aggResult.dValue == null) || (aggResult.dataType == DataType.LONG && aggResult.lValue == null)) {
return Optional.empty();
} else if (aggResult.dataType == DataType.DOUBLE || aggResult.dataType == DataType.LONG) {
if (aggregation == Aggregation.AVG || aggResult.hasDouble) {
double sum = Optional.ofNullable(aggResult.dValue).orElse(0.0d) + Optional.ofNullable(aggResult.lValue).orElse(0L);
return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.SUM ? sum : (sum / aggResult.count))));
DoubleDataEntry doubleDataEntry = new DoubleDataEntry(key, aggregation == Aggregation.SUM ? sum : (sum / aggResult.count));
TsKvEntry result = aggregation == Aggregation.AVG ? new AggTsKvEntry(ts, doubleDataEntry, aggResult.count) : new BasicTsKvEntry(ts, doubleDataEntry);
return Optional.of(result);
} else {
return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggregation == Aggregation.SUM ? aggResult.lValue : (aggResult.lValue / aggResult.count))));
LongDataEntry longDataEntry = new LongDataEntry(key, aggregation == Aggregation.SUM ? aggResult.lValue : (aggResult.lValue / aggResult.count));
return Optional.of(new BasicTsKvEntry(ts, longDataEntry));
}
}
return Optional.empty();
@ -291,5 +301,6 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
Long lValue = null;
long count = 0;
boolean hasDouble = false;
long aggValuesLastTs = 0;
}
}

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

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.dao.entityview.EntityViewService;
@ -45,6 +46,7 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isBlank;
@ -52,6 +54,7 @@ import static org.thingsboard.server.common.data.StringUtils.isBlank;
/**
* @author Andrew Shvayka
*/
@SuppressWarnings("UnstableApiUsage")
@Service
@Slf4j
public class BaseTimeseriesService implements TimeseriesService {
@ -59,7 +62,7 @@ public class BaseTimeseriesService implements TimeseriesService {
private static final int INSERTS_PER_ENTRY = 3;
private static final int INSERTS_PER_ENTRY_WITHOUT_LATEST = 2;
private static final int DELETES_PER_ENTRY = INSERTS_PER_ENTRY;
public static final Function<List<Integer>, Integer> SUM_ALL_INTEGERS = new Function<List<Integer>, Integer>() {
public static final Function<List<Integer>, Integer> SUM_ALL_INTEGERS = new Function<>() {
@Override
public @Nullable Integer apply(@Nullable List<Integer> input) {
int result = 0;
@ -87,7 +90,7 @@ public class BaseTimeseriesService implements TimeseriesService {
private EntityViewService entityViewService;
@Override
public ListenableFuture<List<TsKvEntry>> findAll(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
public ListenableFuture<List<ReadTsKvQueryResult>> findAllByQueries(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
validate(entityId);
queries.forEach(this::validate);
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
@ -103,6 +106,17 @@ public class BaseTimeseriesService implements TimeseriesService {
return timeseriesDao.findAllAsync(tenantId, entityId, queries);
}
@Override
public ListenableFuture<List<TsKvEntry>> findAll(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
return Futures.transform(findAllByQueries(tenantId, entityId, queries),
result -> {
if (result != null && !result.isEmpty()) {
return result.stream().map(ReadTsKvQueryResult::getData).flatMap(Collection::stream).collect(Collectors.toList());
}
return Collections.emptyList();
}, MoreExecutors.directExecutor());
}
@Override
public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
@ -244,7 +258,7 @@ public class BaseTimeseriesService implements TimeseriesService {
public ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId) {
validate(entityId);
return Futures.transformAsync(this.findAllLatest(tenantId, entityId), latest -> {
if (!latest.isEmpty()) {
if (latest != null && !latest.isEmpty()) {
Collection<String> keys = latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList());
return Futures.transform(this.removeLatest(tenantId, entityId, keys), res -> keys, MoreExecutors.directExecutor());
} else {

65
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -42,7 +42,9 @@ import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.TbResultSet;
import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -71,6 +73,7 @@ import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
/**
* @author Andrew Shvayka
*/
@SuppressWarnings("UnstableApiUsage")
@Component
@Slf4j
@NoSqlTsDao
@ -139,20 +142,10 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList());
return Futures.transform(Futures.allAsList(futures), new Function<>() {
@Nullable
@Override
public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) {
if (results == null || results.isEmpty()) {
return null;
}
return results.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
}
}, readResultsProcessingExecutor);
public ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
List<ListenableFuture<ReadTsKvQueryResult>> futures = queries.stream()
.map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList());
return Futures.allAsList(futures);
}
@Override
@ -270,28 +263,42 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(tenantId, entityId, query);
} else {
long startPeriod = query.getStartTs();
long endPeriod = query.getEndTs();
long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs());
long step = Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS);
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>();
while (startPeriod <= endPeriod) {
List<ListenableFuture<Optional<TsKvEntryAggWrapper>>> futures = new ArrayList<>();
while (startPeriod < endPeriod) {
long startTs = startPeriod;
long endTs = Math.min(startPeriod + step, endPeriod + 1);
long endTs = Math.min(startPeriod + step, endPeriod);
long ts = endTs - startTs;
ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, ts, 1, query.getAggregation(), query.getOrder());
futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
startPeriod = endTs;
}
ListenableFuture<List<Optional<TsKvEntry>>> future = Futures.allAsList(futures);
ListenableFuture<List<Optional<TsKvEntryAggWrapper>>> future = Futures.allAsList(futures);
return Futures.transform(future, new Function<>() {
@Nullable
@Override
public List<TsKvEntry> apply(@Nullable List<Optional<TsKvEntry>> input) {
return input == null ? Collections.emptyList() : input.stream().filter(v -> v.isPresent()).map(v -> v.get()).collect(Collectors.toList());
public ReadTsKvQueryResult apply(@Nullable List<Optional<TsKvEntryAggWrapper>> input) {
if (input == null) {
return new ReadTsKvQueryResult(query.getId(), Collections.emptyList(), query.getStartTs());
} else {
long maxTs = query.getStartTs();
List<TsKvEntry> data = new ArrayList<>();
for (var opt : input) {
if (opt.isPresent()) {
TsKvEntryAggWrapper tsKvEntryAggWrapper = opt.get();
maxTs = Math.max(maxTs, tsKvEntryAggWrapper.getLastEntryTs());
data.add(tsKvEntryAggWrapper.getEntry());
}
}
return new ReadTsKvQueryResult(query.getId(), data, maxTs);
}
}
}, readResultsProcessingExecutor);
}
@ -302,13 +309,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
//Cleanup by TTL is native for Cassandra
}
private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
private ListenableFuture<ReadTsKvQueryResult> findAllAsyncWithLimit(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
long minPartition = toPartitionTs(query.getStartTs());
long maxPartition = toPartitionTs(query.getEndTs());
final ListenableFuture<List<Long>> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition);
final SimpleListenableFuture<List<TsKvEntry>> resultFuture = new SimpleListenableFuture<>();
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
Futures.addCallback(partitionsListFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<Long> partitions) {
TsKvQueryCursor cursor = new TsKvQueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions);
@ -321,7 +328,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}
}, readResultsProcessingExecutor);
return resultFuture;
return Futures.transform(resultFuture, tsKvEntries -> {
long lastTs = query.getStartTs();
if (tsKvEntries != null) {
lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs());
}
return new ReadTsKvQueryResult(query.getId(), tsKvEntries, lastTs);
}, MoreExecutors.directExecutor());
}
private long toPartitionTs(long ts) {
@ -379,7 +392,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}
}
private ListenableFuture<Optional<TsKvEntry>> findAndAggregateAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query, long minPartition, long maxPartition) {
private ListenableFuture<Optional<TsKvEntryAggWrapper>> findAndAggregateAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query, long minPartition, long maxPartition) {
final Aggregation aggregation = query.getAggregation();
final String key = query.getKey();
final long startTs = query.getStartTs();

6
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.dao.model.ModelConstants;
@ -145,9 +146,10 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
long endTs = query.getStartTs() - 1;
ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1,
Aggregation.NONE, DESC_ORDER);
ListenableFuture<List<TsKvEntry>> future = aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery);
ListenableFuture<ReadTsKvQueryResult> future = aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery);
return Futures.transformAsync(future, entryList -> {
return Futures.transformAsync(future, result -> {
var entryList = result.getData();
if (entryList.size() == 1) {
TsKvEntry entry = entryList.get(0);
return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor());

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

@ -20,16 +20,18 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.List;
import java.util.Map;
/**
* @author Andrew Shvayka
*/
public interface TimeseriesDao {
ListenableFuture<List<TsKvEntry>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl);

28
dao/src/test/java/org/thingsboard/server/dao/TimescaleDaoServiceTestSuite.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2022 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 org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
import org.junit.runner.RunWith;
@RunWith(ClasspathSuite.class)
@ClassnameFilters({
"org.thingsboard.server.dao.service.*.nosql.*ServiceTimescaleTest",
})
public class TimescaleDaoServiceTestSuite {
}

65
dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java

@ -0,0 +1,65 @@
/**
* Copyright © 2016-2022 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 com.google.common.base.Charsets;
import com.google.common.io.Resources;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
@Slf4j
public class TimescaleSqlInitializer {
private static final List<String> sqlFiles = List.of(
"sql/schema-timescale.sql",
"sql/schema-entities.sql",
"sql/schema-entities-idx.sql",
"sql/schema-entities-idx-psql-addon.sql",
"sql/system-data.sql",
"sql/system-test-psql.sql");
private static final String dropAllTablesSqlFile = "sql/timescale/drop-all-tables.sql";
public static void initDb(Connection conn) {
cleanUpDb(conn);
log.info("initialize Timescale DB...");
try {
for (String sqlFile : sqlFiles) {
URL sqlFileUrl = Resources.getResource(sqlFile);
String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8);
conn.createStatement().execute(sql);
}
} catch (IOException | SQLException e) {
throw new RuntimeException("Unable to init the Timescale database. Reason: " + e.getMessage(), e);
}
log.info("Timescale DB is initialized!");
}
private static void cleanUpDb(Connection conn) {
log.info("clean up Timescale DB...");
try {
URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile);
String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8);
conn.createStatement().execute(dropAllTablesSql);
} catch (IOException | SQLException e) {
throw new RuntimeException("Unable to clean up the Timescale database. Reason: " + e.getMessage(), e);
}
}
}

33
dao/src/test/java/org/thingsboard/server/dao/service/DaoTimescaleTest.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 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.service;
import org.springframework.test.context.TestPropertySource;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@TestPropertySource(locations = {"classpath:application-test.properties", "classpath:timescale-test.properties"})
public @interface DaoTimescaleTest {
}

18
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -213,17 +213,17 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
}
saveEntries(deviceId, TS + 100L + 1L);
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 100, 101, 1, Aggregation.COUNT, DESC_ORDER));
List<ReadTsKvQuery> queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 100, 100, 1, Aggregation.COUNT, DESC_ORDER));
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 11L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 10L)), entries.get(0));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(1, entries.size());
Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 11L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 10L)), entries.get(0));
}
@Test
@ -240,14 +240,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
}
@Test
@ -264,14 +264,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 4L)), entries.get(1));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 3L)), entries.get(1));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 4L)), entries.get(1));
Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 3L)), entries.get(1));
}
@Test
@ -286,14 +286,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 4L)), entries.get(1));
EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0));
Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1));
Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 4L)), entries.get(1));
}
@Test

23
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceTimescaleTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.service.timeseries.nosql;
import org.thingsboard.server.dao.service.DaoTimescaleTest;
import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest;
@DaoTimescaleTest
public class TimeseriesServiceTimescaleTest extends BaseTimeseriesServiceTest {
}

28
dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java

@ -19,8 +19,11 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.Optional;
@ -43,25 +46,25 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
final int LIMIT = 1;
final String TEMP = "temp";
final String DESC = "DESC";
AbstractChunkedAggregationTimeseriesDao tsDao;
private AbstractChunkedAggregationTimeseriesDao tsDao;
@Before
public void setUp() throws Exception {
tsDao = spy(AbstractChunkedAggregationTimeseriesDao.class);
ListenableFuture<Optional<TsKvEntry>> optionalListenableFuture = Futures.immediateFuture(Optional.of(mock(TsKvEntry.class)));
willReturn(optionalListenableFuture).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any());
willReturn(Futures.immediateFuture(mock(TsKvEntry.class))).given(tsDao).getTskvEntriesFuture(any());
Optional<TsKvEntry> optionalListenableFuture = Optional.of(mock(TsKvEntry.class));
willReturn(Futures.immediateFuture(optionalListenableFuture)).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any());
willReturn(Futures.immediateFuture(mock(ReadTsKvQueryResult.class))).given(tsDao).getReadTsKvQueryResultFuture(any(), any());
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenLastIntervalShorterThanOthersAndEqualsEndTs() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2000, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 2001, 1001, LIMIT, COUNT, DESC);
ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 2001, 3001, 2501, LIMIT, COUNT, DESC);
ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 2001, 3000, 2501, LIMIT, COUNT, DESC);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(2)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 2001, getTsForReadTsKvQuery(1, 2001), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 2001, 3000 + 1, getTsForReadTsKvQuery(2001, 3001), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 2001, 3000, getTsForReadTsKvQuery(2001, 3000), COUNT);
}
@Test
@ -71,19 +74,17 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
assertThat(tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query)).isNotNull();
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000 + 1, getTsForReadTsKvQuery(1, 3001), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT);
}
@Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsPeriodMinusOne() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2999, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 3000, 1500, LIMIT, COUNT, DESC);
ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 3000, 3001, 3000, LIMIT, COUNT, DESC);
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(2)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 3000, 3001, getTsForReadTsKvQuery(3000, 3001), COUNT);
}
@ -94,7 +95,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3001, getTsForReadTsKvQuery(1, 3001), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT);
}
@Test
@ -134,7 +135,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3001, getTsForReadTsKvQuery(1, 3001), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT);
}
@Test
@ -144,8 +145,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1000)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
for (long i = 1; i <= 3000; i += 3) {
ReadTsKvQuery querySub = new BaseReadTsKvQuery(TEMP, i, i + 3, i + (i + 3 - i) / 2, LIMIT, COUNT, DESC);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, querySub.getKey(), i, i + 3, getTsForReadTsKvQuery(i, i + 3), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, TEMP, i, Math.min(i + 3, 3000), getTsForReadTsKvQuery(i, i + 3), COUNT);
}
}

18
dao/src/test/resources/timescale-test.properties

@ -0,0 +1,18 @@
database.ts.type=timescale
database.ts_latest.type=timescale
sql.ts_inserts_executor_type=fixed
sql.ts_inserts_fixed_thread_pool_size=10
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.order_by.default_null_ordering=last
spring.jpa.properties.hibernate.jdbc.log.warnings=false
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.url=jdbc:tc:timescaledb:latest-pg12:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.TimescaleSqlInitializer::initDb
spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.hikari.maximumPoolSize = 50

2
pom.xml

@ -133,7 +133,7 @@
<spring-test-dbunit.version>1.3.0</spring-test-dbunit.version> <!-- 2016 -->
<takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 -->
<!-- BLACKBOX TEST SCOPE -->
<testcontainers.version>1.16.0</testcontainers.version>
<testcontainers.version>1.17.3</testcontainers.version>
<zeroturnaround.version>1.12</zeroturnaround.version>
<opensmpp.version>3.0.0</opensmpp.version>
<jgit.version>6.1.0.202203080745-r</jgit.version>

187
ui-ngx/src/app/core/api/data-aggregator.ts

@ -14,7 +14,10 @@
/// limitations under the License.
///
import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models';
import {
AggKey,
IndexedSubscriptionData,
} from '@app/shared/models/telemetry/telemetry.models';
import {
AggregationType,
calculateIntervalComparisonEndTime,
@ -25,10 +28,10 @@ import {
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, isNumber, isNumeric } from '@core/utils';
import { deepClone, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils';
import Timeout = NodeJS.Timeout;
export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void;
export declare type onAggregatedData = (data: IndexedSubscriptionData, detectChanges: boolean) => void;
interface AggData {
count: number;
@ -67,12 +70,12 @@ class AggDataMap {
}
class AggregationMap {
aggMap: {[key: string]: AggDataMap} = {};
aggMap: {[id: number]: AggDataMap} = {};
detectRangeChanged(): boolean {
let changed = false;
for (const key of Object.keys(this.aggMap)) {
const aggDataMap = this.aggMap[key];
for (const id of Object.keys(this.aggMap)) {
const aggDataMap = this.aggMap[id];
if (aggDataMap.rangeChanged) {
changed = true;
aggDataMap.rangeChanged = false;
@ -82,8 +85,8 @@ class AggregationMap {
}
clearRangeChangedFlags() {
for (const key of Object.keys(this.aggMap)) {
this.aggMap[key].rangeChanged = false;
for (const id of Object.keys(this.aggMap)) {
this.aggMap[id].rangeChanged = false;
}
}
}
@ -93,7 +96,7 @@ declare type AggFunction = (aggData: AggData, value?: any) => void;
const avg: AggFunction = (aggData: AggData, value?: any) => {
aggData.count++;
if (isNumber(value)) {
aggData.sum += value;
aggData.sum = aggData.aggValue * (aggData.count - 1) + value;
aggData.aggValue = aggData.sum / aggData.count;
} else {
aggData.aggValue = value;
@ -135,9 +138,25 @@ const none: AggFunction = (aggData: AggData, value?: any) => {
export class DataAggregator {
private dataBuffer: SubscriptionData = {};
private data: SubscriptionData;
private readonly lastPrevKvPairData: {[key: string]: [number, any]};
constructor(private onDataCb: onAggregatedData,
private tsKeys: AggKey[],
private isLatestDataAgg: boolean,
private subsTw: SubscriptionTimewindow,
private utils: UtilsService,
private ignoreDataUpdateOnIntervalTick: boolean) {
this.tsKeys.forEach((key) => {
if (!this.dataBuffer[key.id]) {
this.dataBuffer[key.id] = [];
}
});
if (this.subsTw.aggregation.stateData) {
this.lastPrevKvPairData = {};
}
}
private dataBuffer: IndexedSubscriptionData = [];
private data: IndexedSubscriptionData;
private readonly lastPrevKvPairData: {[id: number]: [number, any]};
private aggregationMap: AggregationMap;
@ -145,9 +164,7 @@ export class DataAggregator {
private resetPending = false;
private updatedData = false;
private noAggregation = this.subsTw.aggregation.type === AggregationType.NONE;
private aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
private readonly aggFunction: AggFunction;
private aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(this.subsTw.aggregation.interval, 1000);
private intervalTimeoutHandle: Timeout;
private intervalScheduledTime: number;
@ -156,38 +173,29 @@ export class DataAggregator {
private endTs: number;
private elapsed: number;
constructor(private onDataCb: onAggregatedData,
private tsKeyNames: string[],
private subsTw: SubscriptionTimewindow,
private utils: UtilsService,
private ignoreDataUpdateOnIntervalTick: boolean) {
this.tsKeyNames.forEach((key) => {
this.dataBuffer[key] = [];
});
if (this.subsTw.aggregation.stateData) {
this.lastPrevKvPairData = {};
private static convertValue(val: string, noAggregation: boolean): any {
if (val && isNumeric(val) && (!noAggregation || noAggregation && Number(val).toString() === val)) {
return Number(val);
}
switch (this.subsTw.aggregation.type) {
return val;
}
private static getAggFunction(aggType: AggregationType): AggFunction {
switch (aggType) {
case AggregationType.MIN:
this.aggFunction = min;
break;
return min;
case AggregationType.MAX:
this.aggFunction = max;
break;
return max;
case AggregationType.AVG:
this.aggFunction = avg;
break;
return avg;
case AggregationType.SUM:
this.aggFunction = sum;
break;
return sum;
case AggregationType.COUNT:
this.aggFunction = count;
break;
return count;
case AggregationType.NONE:
this.aggFunction = none;
break;
return none;
default:
this.aggFunction = avg;
return avg;
}
}
@ -206,7 +214,7 @@ export class DataAggregator {
this.intervalScheduledTime = this.utils.currentPerfTime();
this.calculateStartEndTs();
this.elapsed = 0;
this.aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
this.aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(this.subsTw.aggregation.interval, 1000);
this.resetPending = true;
this.updatedData = false;
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
@ -220,7 +228,7 @@ export class DataAggregator {
this.aggregationMap = null;
}
public onData(data: SubscriptionDataHolder, update: boolean, history: boolean, detectChanges: boolean) {
public onData(data: IndexedSubscriptionData, update: boolean, history: boolean, detectChanges: boolean) {
this.updatedData = true;
if (!this.dataReceived || this.resetPending) {
let updateIntervalScheduledTime = true;
@ -235,9 +243,9 @@ export class DataAggregator {
}
if (update) {
this.aggregationMap = new AggregationMap();
this.updateAggregatedData(data.data);
this.updateAggregatedData(data);
} else {
this.aggregationMap = this.processAggregatedData(data.data);
this.aggregationMap = this.processAggregatedData(data);
}
if (updateIntervalScheduledTime) {
this.intervalScheduledTime = this.utils.currentPerfTime();
@ -245,7 +253,7 @@ export class DataAggregator {
this.aggregationMap.clearRangeChangedFlags();
this.onInterval(history, detectChanges);
} else {
this.updateAggregatedData(data.data);
this.updateAggregatedData(data);
if (history) {
this.intervalScheduledTime = this.utils.currentPerfTime();
this.onInterval(history, detectChanges);
@ -283,9 +291,9 @@ export class DataAggregator {
}
const intervalTimeout = rangeChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout;
if (!history) {
const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval);
const delta = Math.floor(this.elapsed / this.aggregationTimeout);
if (delta || !this.data || rangeChanged) {
const tickTs = delta * this.subsTw.aggregation.interval;
const tickTs = delta * this.aggregationTimeout;
if (this.subsTw.quickInterval) {
const startEndTime = calculateIntervalStartEndTime(this.subsTw.quickInterval, this.subsTw.timezone);
this.startTs = startEndTime[0] + this.subsTw.tsOffset;
@ -295,7 +303,7 @@ export class DataAggregator {
this.endTs += tickTs;
}
this.data = this.updateData();
this.elapsed = this.elapsed - delta * this.subsTw.aggregation.interval;
this.elapsed = this.elapsed - delta * this.aggregationTimeout;
}
} else {
this.data = this.updateData();
@ -309,39 +317,45 @@ export class DataAggregator {
}
}
private updateData(): SubscriptionData {
this.tsKeyNames.forEach((key) => {
this.dataBuffer[key] = [];
private updateData(): IndexedSubscriptionData {
this.dataBuffer = [];
this.tsKeys.forEach((key) => {
if (!this.dataBuffer[key.id]) {
this.dataBuffer[key.id] = [];
}
});
for (const key of Object.keys(this.aggregationMap.aggMap)) {
const aggKeyData = this.aggregationMap.aggMap[key];
let keyData = this.dataBuffer[key];
for (const idStr of Object.keys(this.aggregationMap.aggMap)) {
const id = Number(idStr);
const aggKeyData = this.aggregationMap.aggMap[id];
const aggKey = this.aggKeyById(id);
const noAggregation = aggKey.agg === AggregationType.NONE;
let keyData = this.dataBuffer[id];
aggKeyData.forEach((aggData, aggTimestamp) => {
if (aggTimestamp < this.startTs) {
if (this.subsTw.aggregation.stateData &&
(!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) {
this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue];
(!this.lastPrevKvPairData[id] || this.lastPrevKvPairData[id][0] < aggTimestamp)) {
this.lastPrevKvPairData[id] = [aggTimestamp, aggData.aggValue];
}
aggKeyData.delete(aggTimestamp);
this.updatedData = true;
} else if (aggTimestamp < this.endTs || this.noAggregation) {
} else if (aggTimestamp < this.endTs || noAggregation) {
const kvPair: [number, any] = [aggTimestamp, aggData.aggValue];
keyData.push(kvPair);
}
});
keyData.sort((set1, set2) => set1[0] - set2[0]);
if (this.subsTw.aggregation.stateData) {
this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key]));
this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[id]));
}
if (keyData.length > this.subsTw.aggregation.limit) {
keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit);
}
this.dataBuffer[key] = keyData;
this.dataBuffer[id] = keyData;
}
return this.dataBuffer;
}
private updateStateBounds(keyData: [number, any][], lastPrevKvPair: [number, any]) {
private updateStateBounds(keyData: [number, any, number?][], lastPrevKvPair: [number, any]) {
if (lastPrevKvPair) {
lastPrevKvPair[0] = this.startTs;
}
@ -369,66 +383,71 @@ export class DataAggregator {
}
}
private processAggregatedData(data: SubscriptionData): AggregationMap {
const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
private processAggregatedData(data: IndexedSubscriptionData): AggregationMap {
const aggregationMap = new AggregationMap();
for (const key of Object.keys(data)) {
let aggKeyData = aggregationMap.aggMap[key];
for (const idStr of Object.keys(data)) {
const id = Number(idStr);
const aggKey = this.aggKeyById(id);
const aggType = aggKey.agg;
const isCount = aggType === AggregationType.COUNT;
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap();
aggregationMap.aggMap[key] = aggKeyData;
aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[key];
const keyData = data[id];
keyData.forEach((kvPair) => {
const timestamp = kvPair[0];
const value = this.convertValue(kvPair[1]);
const aggKey = timestamp;
const value = DataAggregator.convertValue(kvPair[1], noAggregation);
const tsKey = timestamp;
const aggData = {
count: isCount ? value : 1,
count: isCount ? value : isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1,
sum: value,
aggValue: value
};
aggKeyData.set(aggKey, aggData);
aggKeyData.set(tsKey, aggData);
});
}
return aggregationMap;
}
private updateAggregatedData(data: SubscriptionData) {
const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
for (const key of Object.keys(data)) {
let aggKeyData = this.aggregationMap.aggMap[key];
private updateAggregatedData(data: IndexedSubscriptionData) {
for (const idStr of Object.keys(data)) {
const id = Number(idStr);
const aggKey = this.aggKeyById(id);
const aggType = aggKey.agg;
const isCount = aggType === AggregationType.COUNT;
const noAggregation = aggType === AggregationType.NONE;
let aggKeyData = this.aggregationMap.aggMap[id];
if (!aggKeyData) {
aggKeyData = new AggDataMap();
this.aggregationMap.aggMap[key] = aggKeyData;
this.aggregationMap.aggMap[id] = aggKeyData;
}
const keyData = data[key];
const keyData = data[id];
keyData.forEach((kvPair) => {
const timestamp = kvPair[0];
const value = this.convertValue(kvPair[1]);
const aggTimestamp = this.noAggregation ? timestamp : (this.startTs +
const value = DataAggregator.convertValue(kvPair[1], noAggregation);
const aggTimestamp = noAggregation ? timestamp : (this.startTs +
Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) *
this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2);
let aggData = aggKeyData.get(aggTimestamp);
if (!aggData) {
aggData = {
count: 1,
count: isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1,
sum: value,
aggValue: isCount ? 1 : value
};
aggKeyData.set(aggTimestamp, aggData);
} else {
this.aggFunction(aggData, value);
DataAggregator.getAggFunction(aggType)(aggData, value);
}
});
}
}
private convertValue(val: string): any {
if (val && isNumeric(val) && (!this.noAggregation || this.noAggregation && Number(val).toString() === val)) {
return Number(val);
}
return val;
private aggKeyById(id: number): AggKey {
return this.tsKeys.find(key => key.id === id);
}
}

564
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -14,9 +14,16 @@
/// limitations under the License.
///
import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models';
import { ComparisonResultType, DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
import {
AggregationType,
ComparisonDuration,
createTimewindowForComparison,
getCurrentTime,
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import {
ComparisonTsValue,
EntityData,
EntityDataPageLink,
EntityFilter,
@ -28,11 +35,12 @@ import {
TsValue
} from '@shared/models/query/query.models';
import {
AggKey,
DataKeyType,
EntityCountCmd,
EntityDataCmd,
IndexedSubscriptionData,
SubscriptionData,
SubscriptionDataHolder,
TelemetryService,
TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models';
@ -55,6 +63,11 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number,
export interface SubscriptionDataKey {
name: string;
type: DataKeyType;
aggregationType?: AggregationType;
comparisonEnabled?: boolean;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
comparisonResultType?: ComparisonResultType;
funcBody: string;
func?: DataKeyFunction;
postFuncBody: string;
@ -82,9 +95,16 @@ export interface EntityDataSubscriptionOptions {
export class EntityDataSubscription {
constructor(private listener: EntityDataListener,
private telemetryService: TelemetryService,
private utils: UtilsService) {
this.initializeSubscription();
}
private entityDataSubscriptionOptions = this.listener.subscriptionOptions;
private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType;
private history: boolean;
private isFloatingTimewindow: boolean;
private realtime: boolean;
private subscriber: TelemetrySubscriber;
@ -95,13 +115,18 @@ export class EntityDataSubscription {
private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>;
private latestValues: Array<EntityKey>;
private aggTsValues: Array<AggKey>;
private aggTsComparisonValues: Array<AggKey>;
private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>;
private data: Array<Array<DataSetHolder>>;
private subsTw: SubscriptionTimewindow;
private latestTsOffset: number;
private dataAggregators: Array<DataAggregator>;
private tsLatestDataAggregators: Array<DataAggregator>;
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
private dataKeysList: SubscriptionDataKey[] = [];
private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}};
private entityIdToDataIndex: {[id: string]: number};
@ -116,15 +141,44 @@ export class EntityDataSubscription {
private dataResolved = false;
private started = false;
constructor(private listener: EntityDataListener,
private telemetryService: TelemetryService,
private utils: UtilsService) {
this.initializeSubscription();
private static convertValue(val: string): any {
if (val && isNumeric(val) && Number(val).toString() === val) {
return Number(val);
}
return val;
}
private static calculateComparisonValue(key: SubscriptionDataKey, comparisonTsValue: ComparisonTsValue): [number, any, number?][] {
let timestamp: number;
let value: any;
switch (key.comparisonResultType) {
case ComparisonResultType.PREVIOUS_VALUE:
timestamp = comparisonTsValue.previous.ts;
value = comparisonTsValue.previous.value;
break;
case ComparisonResultType.DELTA_ABSOLUTE:
case ComparisonResultType.DELTA_PERCENT:
timestamp = comparisonTsValue.previous.ts;
const currentVal = EntityDataSubscription.convertValue(comparisonTsValue.current.value);
const prevVal = EntityDataSubscription.convertValue(comparisonTsValue.previous.value);
if (isNumeric(currentVal) && isNumeric(prevVal)) {
if (key.comparisonResultType === ComparisonResultType.DELTA_ABSOLUTE) {
value = currentVal - prevVal;
} else {
value = (currentVal - prevVal) / prevVal * 100;
}
} else {
value = '';
}
break;
}
return [[timestamp, value]];
}
private initializeSubscription() {
for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) {
const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]);
this.dataKeysList.push(dataKey);
dataKey.index = i;
if (this.datasourceType === DatasourceType.function) {
if (!dataKey.func) {
@ -142,7 +196,8 @@ export class EntityDataSubscription {
if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
} else {
key = `${dataKey.name}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
const keyIndexSuffix = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE ? `_${dataKey.index}` : '';
key = `${dataKey.name}_${dataKey.type}${keyIndexSuffix}${dataKey.latest ? '_latest' : ''}`;
}
let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
if (!dataKeysList) {
@ -180,6 +235,12 @@ export class EntityDataSubscription {
});
this.dataAggregators = null;
}
if (this.tsLatestDataAggregators) {
this.tsLatestDataAggregators.forEach((aggregator) => {
aggregator.destroy();
});
this.tsLatestDataAggregators = null;
}
this.pageData = null;
}
@ -188,16 +249,11 @@ export class EntityDataSubscription {
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.started = true;
this.dataResolved = true;
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
this.prepareSubscriptionTimewindow();
}
if (this.datasourceType === DatasourceType.entity) {
const entityFields: Array<EntityKey> =
this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map(
dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
);
if (!entityFields.find(key => key.key === 'name')) {
@ -219,18 +275,20 @@ export class EntityDataSubscription {
});
}
this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map(
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
);
this.tsFields = this.entityDataSubscriptionOptions.dataKeys.
filter(dataKey => dataKey.type === DataKeyType.timeseries && !dataKey.latest).map(
this.tsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
);
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const latestTsFields = this.entityDataSubscriptionOptions.dataKeys.
filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest).map(
const latestTsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
);
this.latestValues = this.attrFields.concat(latestTsFields);
@ -238,6 +296,19 @@ export class EntityDataSubscription {
this.latestValues = this.attrFields.concat(this.tsFields);
}
this.aggTsValues = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && !dataKey.comparisonEnabled).map(
dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType })
);
this.aggTsComparisonValues = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map(
dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType,
previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE })
);
this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd();
@ -282,19 +353,28 @@ export class EntityDataSubscription {
this.subscriber.reconnect$.subscribe(() => {
if (this.started) {
const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries &&
!this.history && this.tsFields.length) {
if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length ||
this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) {
const newSubsTw = this.listener.updateRealtimeSubscription();
this.subsTw = newSubsTw;
targetCommand.tsCmd.startTs = this.subsTw.startTs;
targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
targetCommand.tsCmd.interval = this.subsTw.aggregation.interval;
targetCommand.tsCmd.limit = this.subsTw.aggregation.limit;
targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
this.dataAggregators.forEach((dataAggregator) => {
dataAggregator.reset(newSubsTw);
});
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) {
targetCommand.tsCmd.startTs = this.subsTw.startTs;
targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
targetCommand.tsCmd.interval = this.subsTw.aggregation.interval;
targetCommand.tsCmd.limit = this.subsTw.aggregation.limit;
targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
this.dataAggregators.forEach((dataAggregator) => {
dataAggregator.reset(newSubsTw);
});
}
if (this.aggTsValues.length > 0 && !this.isFloatingTimewindow) {
targetCommand.aggTsCmd.startTs = this.subsTw.startTs;
targetCommand.aggTsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
this.tsLatestDataAggregators.forEach((dataAggregator) => {
dataAggregator.reset(newSubsTw);
});
}
}
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.subscriber.setTsOffset(this.subsTw.tsOffset);
@ -359,7 +439,7 @@ export class EntityDataSubscription {
entityType: null
};
const countKey = this.entityDataSubscriptionOptions.dataKeys[0];
const countKey = this.dataKeysList[0];
let dataReceived = false;
@ -422,14 +502,9 @@ export class EntityDataSubscription {
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return;
}
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
this.prepareSubscriptionTimewindow();
this.prepareData();
this.prepareData(true);
if (this.datasourceType === DatasourceType.entity) {
this.subsCommand = new EntityDataCmd();
@ -461,7 +536,19 @@ export class EntityDataSubscription {
this.started = true;
}
private prepareSubscriptionTimewindow() {
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow;
this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow);
this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs);
this.isFloatingTimewindow = this.entityDataSubscriptionOptions.subscriptionTimewindow &&
!this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval && !this.history;
}
private prepareSubscriptionCommands(cmd: EntityDataCmd) {
let latestValuesKeys: EntityKey[] = [];
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.tsFields.length > 0) {
if (this.history) {
@ -486,17 +573,40 @@ export class EntityDataSubscription {
};
}
}
if (this.latestValues.length > 0) {
cmd.latestCmd = {
keys: this.latestValues
};
}
latestValuesKeys = this.latestValues;
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.latestValues.length > 0) {
cmd.latestCmd = {
keys: this.latestValues
};
latestValuesKeys = this.latestValues;
}
if (this.history && (this.aggTsValues.length > 0 || this.aggTsComparisonValues.length > 0)) {
for (const aggTsComparison of this.aggTsComparisonValues) {
const subscriptionDataKey = this.dataKeyByIndex(aggTsComparison.id);
const timewindowForComparison =
createTimewindowForComparison(this.subsTw, subscriptionDataKey.timeForComparison,
subscriptionDataKey.comparisonCustomIntervalValue);
aggTsComparison.previousStartTs = timewindowForComparison.fixedWindow.startTimeMs;
aggTsComparison.previousEndTs = timewindowForComparison.fixedWindow.endTimeMs;
}
cmd.aggHistoryCmd = {
keys: [...this.aggTsValues, ...this.aggTsComparisonValues],
startTs: this.subsTw.fixedWindow.startTimeMs,
endTs: this.subsTw.fixedWindow.endTimeMs
};
} else if (!this.isFloatingTimewindow && this.aggTsValues.length > 0) {
cmd.aggTsCmd = {
keys: this.aggTsValues,
startTs: this.subsTw.startTs,
timeWindow: this.subsTw.aggregation.timeWindow
};
if (latestValuesKeys.length > 0) {
const tsKeys = this.aggTsValues.map(key => key.key);
latestValuesKeys = latestValuesKeys.filter(latestKey => latestKey.type !== EntityKeyType.TIME_SERIES
|| !tsKeys.includes(latestKey.key));
}
}
if (latestValuesKeys.length > 0) {
cmd.latestCmd = {
keys: latestValuesKeys
};
}
}
@ -510,7 +620,7 @@ export class EntityDataSubscription {
this.generateData(true);
}
private prepareData() {
private prepareData(isUpdate: boolean) {
if (this.timeseriesTimer) {
clearTimeout(this.timeseriesTimer);
this.timeseriesTimer = null;
@ -526,37 +636,73 @@ export class EntityDataSubscription {
});
}
this.dataAggregators = [];
if (this.tsLatestDataAggregators) {
this.tsLatestDataAggregators.forEach((aggregator) => {
aggregator.destroy();
});
}
this.tsLatestDataAggregators = [];
this.resetData();
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
let tsKeyNames = [];
let tsKeyIds: number[];
if (this.datasourceType === DatasourceType.function) {
for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeysList.forEach((subscriptionDataKey) => {
if (!subscriptionDataKey.latest) {
tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`);
}
});
}
tsKeyIds = this.dataKeysList.filter(key => !key.latest).map(key => key.index);
} else {
tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : [];
tsKeyIds = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map(
dataKey => dataKey.index
);
}
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
if (tsKeyNames.length) {
if (this.datasourceType === DatasourceType.function) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.function, dataIndex, this.notifyListener.bind(this));
} else {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
}
const aggKeys: AggKey[] = tsKeyIds.map(key => ({id: key, key: key + '', agg: this.subsTw.aggregation.type}));
if (aggKeys.length) {
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys,
false, dataIndex, this.notifyListener.bind(this));
}
}
}
if (this.aggTsValues && this.aggTsValues.length) {
if (!this.isFloatingTimewindow) {
const aggLatestTimewindow = deepClone(this.subsTw);
aggLatestTimewindow.aggregation.stateData = false;
aggLatestTimewindow.aggregation.interval = aggLatestTimewindow.aggregation.timeWindow;
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
this.tsLatestDataAggregators[dataIndex] = this.createRealtimeDataAggregator(aggLatestTimewindow, this.aggTsValues,
true, dataIndex, this.notifyListener.bind(this));
}
} else {
this.reportNotSupported(this.aggTsValues, isUpdate);
}
}
if (!this.history && this.aggTsComparisonValues && this.aggTsComparisonValues.length) {
this.reportNotSupported(this.aggTsComparisonValues, isUpdate);
}
}
private reportNotSupported(keys: AggKey[], isUpdate: boolean) {
const indexedData: IndexedSubscriptionData = [];
for (const key of keys) {
indexedData[key.id] = [[0, 'Not supported!']];
}
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
this.onIndexedData(indexedData, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries,
(data, dataIndex1, dataKeyIndex, detectChanges, isLatest) => {
if (!this.data[dataIndex1]) {
this.data[dataIndex1] = [];
}
this.data[dataIndex1][dataKeyIndex] = data;
if (isUpdate) {
this.notifyListener(data, dataIndex1, dataKeyIndex, detectChanges, isLatest);
}
});
}
}
private resetData() {
this.data = [];
this.datasourceData = [];
this.entityIdToDataIndex = {};
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
@ -609,19 +755,18 @@ export class EntityDataSubscription {
this.pageData = pageData;
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.prepareData();
this.prepareData(false);
} else if (isInitialData) {
this.resetData();
}
const data: Array<Array<DataSetHolder>> = [];
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
const entityData = pageData.data[dataIndex];
this.processEntityData(entityData, dataIndex, false,
(data1, dataIndex1, dataKeyIndex) => {
if (!data[dataIndex1]) {
data[dataIndex1] = [];
if (!this.data[dataIndex1]) {
this.data[dataIndex1] = [];
}
data[dataIndex1][dataKeyIndex] = data1;
this.data[dataIndex1][dataKeyIndex] = data1;
}
);
}
@ -630,7 +775,7 @@ export class EntityDataSubscription {
this.entityDataResolveSubject.next(
{
pageData,
data,
data: this.data,
datasourceIndex: this.listener.configDatasourceIndex,
pageLink: this.entityDataSubscriptionOptions.pageLink
}
@ -638,7 +783,7 @@ export class EntityDataSubscription {
this.entityDataResolveSubject.complete();
} else {
if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.listener.dataLoaded(pageData, data,
this.listener.dataLoaded(pageData, this.data,
this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink);
}
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) {
@ -648,7 +793,7 @@ export class EntityDataSubscription {
this.entityDataResolveSubject.next(
{
pageData,
data,
data: this.data,
datasourceIndex: this.listener.configDatasourceIndex,
pageLink: this.entityDataSubscriptionOptions.pageLink
}
@ -675,27 +820,85 @@ export class EntityDataSubscription {
private processEntityData(entityData: EntityData, dataIndex: number, isUpdate: boolean,
dataUpdatedCb: DataUpdatedCb) {
if ((this.entityDataSubscriptionOptions.type === widgetType.latest ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) && entityData.latest) {
for (const type of Object.keys(entityData.latest)) {
const subscriptionData = this.toSubscriptionData(entityData.latest[type], false);
const dataKeyType = entityKeyTypeToDataKeyType(EntityKeyType[type]);
this.onData(subscriptionData, dataKeyType, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
if (this.entityDataSubscriptionOptions.type === widgetType.latest ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (entityData.aggLatest) {
const aggData: IndexedSubscriptionData = [];
for (const idStr of Object.keys(entityData.aggLatest)) {
const id = Number(idStr);
const dataKey = this.dataKeyByIndex(id);
const aggLatestData = entityData.aggLatest[id];
if (dataKey.comparisonEnabled) {
const keyData = EntityDataSubscription.calculateComparisonValue(dataKey, aggLatestData);
this.onKeyData(keyData, dataKey.name, id, dataKey.type, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, true, dataUpdatedCb);
} else {
aggData[id] = [[aggLatestData.current.ts, aggLatestData.current.value, aggLatestData.current.count]];
}
}
if (Object.keys(aggData).length > 0 && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) {
const dataAggregator = this.tsLatestDataAggregators[dataIndex];
let prevDataCb;
if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onIndexedData(data, dataIndex, detectChanges,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
});
}
dataAggregator.onData(aggData, false, this.history, true);
if (prevDataCb) {
dataAggregator.updateOnDataCb(prevDataCb);
}
}
}
if (entityData.latest) {
for (const type of Object.keys(entityData.latest)) {
const subscriptionData = this.toSubscriptionData(entityData.latest[type], false);
const dataKeyType = entityKeyTypeToDataKeyType(EntityKeyType[type]);
if (isUpdate && EntityKeyType[type] === EntityKeyType.TIME_SERIES) {
const keys: string[] = Object.keys(subscriptionData);
const latestTsKeys = this.latestValues.filter(key => key.type === EntityKeyType.TIME_SERIES && keys.includes(key.key));
if (latestTsKeys.length) {
const latestTsSubsciptionData: SubscriptionData = {};
for (const latestTsKey of latestTsKeys) {
latestTsSubsciptionData[latestTsKey.key] = subscriptionData[latestTsKey.key];
}
this.onData(latestTsSubsciptionData, dataKeyType, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
}
const aggTsKeys = this.aggTsValues.filter(key => keys.includes(key.key));
if (!this.history && aggTsKeys.length && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) {
const dataAggregator = this.tsLatestDataAggregators[dataIndex];
const indexedData: IndexedSubscriptionData = [];
for (const aggKey of aggTsKeys) {
indexedData[aggKey.id] = subscriptionData[aggKey.key];
}
dataAggregator.onData(indexedData, true, false, true);
}
} else {
this.onData(subscriptionData, dataKeyType, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
}
}
}
}
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && entityData.timeseries) {
const subscriptionData = this.toSubscriptionData(entityData.timeseries, true);
if (this.dataAggregators && this.dataAggregators[dataIndex]) {
const dataAggregator = this.dataAggregators[dataIndex];
const keyNames = Object.keys(subscriptionData);
const dataKeys = this.timeseriesDataKeysByKeyNames(keyNames);
const indexedData: IndexedSubscriptionData = [];
for (const dataKey of dataKeys) {
indexedData[dataKey.index] = subscriptionData[dataKey.name];
}
let prevDataCb;
if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onData(data, this.datasourceType === DatasourceType.function ?
DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, false, dataUpdatedCb);
this.onIndexedData(data, dataIndex, detectChanges, false, dataUpdatedCb);
});
}
dataAggregator.onData({data: subscriptionData}, false, this.history, true);
dataAggregator.onData(indexedData, false, this.history, true);
if (prevDataCb) {
dataAggregator.updateOnDataCb(prevDataCb);
}
@ -707,92 +910,109 @@ export class EntityDataSubscription {
private onData(sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean,
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const keyName of Object.keys(sourceData)) {
const keyData = sourceData[keyName];
const key = `${keyName}_${type}${isTsLatest ? '_latest' : ''}`;
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) {
const datasourceKey = `${key}_${keyIndex}`;
if (this.datasourceData[dataIndex][datasourceKey].data) {
const dataKey = dataKeyList[keyIndex];
const data: DataSet = [];
let prevSeries: [number, any];
let prevOrigSeries: [number, any];
let datasourceKeyData: DataSet;
let datasourceOrigKeyData: DataSet;
let update = false;
if (this.realtime && !isTsLatest) {
datasourceKeyData = [];
datasourceOrigKeyData = [];
} else {
datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data;
datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data;
}
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1];
} else {
prevSeries = [0, 0];
prevOrigSeries = [0, 0];
}
this.datasourceOrigData[dataIndex][datasourceKey].data = [];
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) {
keyData.forEach((keySeries) => {
let series = keySeries;
const time = series[0];
this.datasourceOrigData[dataIndex][datasourceKey].data.push(series);
let value = this.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
prevOrigSeries = series;
series = [time, value];
data.push(series);
prevSeries = series;
});
update = true;
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) {
if (keyData.length > 0) {
let series = keyData[0];
const time = series[0];
this.datasourceOrigData[dataIndex][datasourceKey].data.push(series);
let value = this.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
series = [time, value];
data.push(series);
}
update = true;
}
if (update) {
this.datasourceData[dataIndex][datasourceKey].data = data;
dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest);
}
}
for (const key of Object.keys(sourceData)) {
const keyData = sourceData[key];
this.onKeyData(keyData, key, 0, type,
dataIndex, detectChanges, isTsLatest, false, dataUpdatedCb);
}
}
private onIndexedData(sourceData: IndexedSubscriptionData, dataIndex: number, detectChanges: boolean,
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const indexStr of Object.keys(sourceData)) {
const id = Number(indexStr);
const dataKey = this.dataKeyByIndex(id);
const isAggLatest = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE;
const keyData = sourceData[id];
let keyName = dataKey.name;
if (dataKey.type === DataKeyType.function) {
keyName += `_${dataKey.index}`;
}
this.onKeyData(keyData, keyName, id, dataKey.type,
dataIndex, detectChanges, isTsLatest, isAggLatest, dataUpdatedCb);
}
}
private convertValue(val: string): any {
if (val && isNumeric(val) && Number(val).toString() === val) {
return Number(val);
private onKeyData(keyData: [number, any, number?][], keyName: string, id: number, type: DataKeyType,
dataIndex: number, detectChanges: boolean,
isTsLatest: boolean, isAggLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
const keyIdSuffix = isAggLatest ? `_${id}` : '';
const key = `${keyName}_${type}${keyIdSuffix}${isTsLatest ? '_latest' : ''}`;
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) {
const datasourceKey = `${key}_${keyIndex}`;
if (this.datasourceData[dataIndex][datasourceKey].data) {
const dataKey = dataKeyList[keyIndex];
const data: DataSet = [];
let prevSeries: [number, any];
let prevOrigSeries: [number, any];
let datasourceKeyData: DataSet;
let datasourceOrigKeyData: DataSet;
let update = false;
if (this.realtime && !isTsLatest) {
datasourceKeyData = [];
datasourceOrigKeyData = [];
} else {
datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data;
datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data;
}
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1];
} else {
prevSeries = [0, 0];
prevOrigSeries = [0, 0];
}
this.datasourceOrigData[dataIndex][datasourceKey].data = [];
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) {
keyData.forEach((keySeries) => {
let series = keySeries;
const time = series[0];
this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]);
let value = EntityDataSubscription.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
prevOrigSeries = [series[0], series[1]];
series = [series[0], value];
data.push([series[0], series[1]]);
prevSeries = [series[0], series[1]];
});
update = true;
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) {
if (keyData.length > 0) {
let series = keyData[0];
const time = series[0];
this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]);
let value = EntityDataSubscription.convertValue(series[1]);
if (dataKey.postFunc) {
value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]);
}
series = [time, value];
data.push([series[0], series[1]]);
}
update = true;
}
if (update) {
this.datasourceData[dataIndex][datasourceKey].data = data;
dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest);
}
}
}
return val;
}
private toSubscriptionData(sourceData: {[key: string]: TsValue | TsValue[]}, isTs: boolean): SubscriptionData {
const subsData: SubscriptionData = {};
for (const keyName of Object.keys(sourceData)) {
const values = sourceData[keyName];
const dataSet: [number, any][] = [];
const dataSet: [number, any, number?][] = [];
if (isTs) {
(values as TsValue[]).forEach((keySeries) => {
dataSet.push([keySeries.ts, keySeries.value]);
dataSet.push([keySeries.ts, keySeries.value, keySeries.count]);
});
} else {
const tsValue = values as TsValue;
dataSet.push([tsValue.ts, tsValue.value]);
dataSet.push([tsValue.ts, tsValue.value, tsValue.count]);
}
subsData[keyName] = dataSet;
}
@ -800,21 +1020,37 @@ export class EntityDataSubscription {
}
private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow,
tsKeyNames: Array<string>,
dataKeyType: DataKeyType,
tsKeys: Array<AggKey>,
isLatestDataAgg: boolean,
dataIndex: number,
dataUpdatedCb: DataUpdatedCb): DataAggregator {
return new DataAggregator(
(data, detectChanges) => {
this.onData(data, dataKeyType, dataIndex, detectChanges, false, dataUpdatedCb);
this.onIndexedData(data, dataIndex, detectChanges,
isLatestDataAgg && (this.entityDataSubscriptionOptions.type === widgetType.timeseries), dataUpdatedCb);
},
tsKeyNames,
tsKeys,
isLatestDataAgg,
subsTw,
this.utils,
this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick
this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick || isLatestDataAgg
);
}
private dataKeyByIndex(index: number): SubscriptionDataKey {
return this.dataKeysList.find(key => key.index === index);
}
private timeseriesDataKeysByKeyNames(keyNames: string[]): SubscriptionDataKey[] {
const result: SubscriptionDataKey[] = [];
for (const keyName of keyNames) {
const key = `${keyName}_${DataKeyType.timeseries}`;
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
result.push(...dataKeyList);
}
return result;
}
private generateSeries(dataKey: SubscriptionDataKey, startTime: number, endTime: number): [number, any][] {
const data: [number, any][] = [];
let prevSeries: [number, any];
@ -893,9 +1129,7 @@ export class EntityDataSubscription {
let startTime: number;
let endTime: number;
let delta: number;
const generatedData: SubscriptionDataHolder = {
data: {}
};
const generatedData: IndexedSubscriptionData = [];
if (!this.history) {
delta = Math.floor(this.tickElapsed / this.frequency);
}
@ -928,7 +1162,7 @@ export class EntityDataSubscription {
endTime = Math.min(currentTime, endTime);
}
}
generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, startTime, endTime);
generatedData[dataKey.index] = this.generateSeries(dataKey, startTime, endTime);
}
if (this.dataAggregators && this.dataAggregators.length) {
this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges);

42
ui-ngx/src/app/core/api/entity-data.service.ts

@ -31,6 +31,7 @@ import { Observable, of } from 'rxjs';
export interface EntityDataListener {
subscriptionType: widgetType;
useTimewindow?: boolean;
subscriptionTimewindow?: SubscriptionTimewindow;
latestTsOffset?: number;
configDatasource: Datasource;
@ -73,6 +74,21 @@ export class EntityDataService {
}
}
private static toSubscriptionDataKey(dataKey: DataKey, latest: boolean): SubscriptionDataKey {
return {
name: dataKey.name,
type: dataKey.type,
aggregationType: dataKey.aggregationType,
comparisonEnabled: dataKey.comparisonEnabled,
timeForComparison: dataKey.timeForComparison,
comparisonCustomIntervalValue: dataKey.comparisonCustomIntervalValue,
comparisonResultType: dataKey.comparisonResultType,
funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody,
latest
};
}
public prepareSubscription(listener: EntityDataListener,
ignoreDataUpdateOnIntervalTick = false): Observable<EntityDataLoadResult> {
const datasource = listener.configDatasource;
@ -93,10 +109,10 @@ export class EntityDataService {
public startSubscription(listener: EntityDataListener) {
if (listener.subscription) {
if (listener.subscriptionType === widgetType.timeseries) {
if (listener.useTimewindow) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} else if (listener.subscriptionType === widgetType.latest) {
}
if (listener.subscriptionType === widgetType.timeseries || listener.subscriptionType === widgetType.latest) {
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
}
listener.subscription.start();
@ -122,10 +138,10 @@ export class EntityDataService {
return of(null);
}
listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils);
if (listener.subscriptionType === widgetType.timeseries) {
if (listener.useTimewindow) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} else if (listener.subscriptionType === widgetType.latest) {
}
if (listener.subscriptionType === widgetType.timeseries || listener.subscriptionType === widgetType.latest) {
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
}
return listener.subscription.subscribe();
@ -146,11 +162,11 @@ export class EntityDataService {
ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions {
const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
datasource.dataKeys.forEach((dataKey) => {
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, false));
subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, false));
});
if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => {
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, true));
subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, true));
});
}
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
@ -171,14 +187,4 @@ export class EntityDataService {
entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick;
return entityDataSubscriptionOptions;
}
private toSubscriptionDataKey(dataKey: DataKey, latest: boolean): SubscriptionDataKey {
return {
name: dataKey.name,
type: dataKey.type,
funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody,
latest
};
}
}

49
ui-ngx/src/app/core/api/widget-subscription.ts

@ -28,6 +28,7 @@ import {
DataSetHolder,
Datasource,
DatasourceData,
datasourcesHasAggregation,
DatasourceType,
LegendConfig,
LegendData,
@ -95,6 +96,7 @@ export class WidgetSubscription implements IWidgetSubscription {
timezone: string;
subscriptionTimewindow: SubscriptionTimewindow;
useDashboardTimewindow: boolean;
useTimewindow: boolean;
tsOffset = 0;
hasDataPageLink: boolean;
@ -200,6 +202,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.originalTimewindow = null;
this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow;
this.useTimewindow = true;
if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else {
@ -245,15 +248,16 @@ export class WidgetSubscription implements IWidgetSubscription {
this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow;
this.stateData = options.stateData;
if (this.type === widgetType.latest) {
this.timezone = options.dashboardTimewindow.timezone;
this.updateTsOffset();
}
this.useTimewindow = this.type === widgetType.timeseries || datasourcesHasAggregation(this.configuredDatasources);
if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else {
this.timeWindowConfig = deepClone(options.timeWindowConfig);
}
if (this.type === widgetType.latest) {
this.timezone = this.useTimewindow ? this.timeWindowConfig.timezone : options.dashboardTimewindow.timezone;
this.updateTsOffset();
}
this.subscriptionTimewindow = null;
this.comparisonEnabled = options.comparisonEnabled && isHistoryTypeTimewindow(this.timeWindowConfig);
@ -443,6 +447,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const resolveResultObservables = this.configuredDatasources.map((datasource, index) => {
const listener: EntityDataListener = {
subscriptionType: this.type,
useTimewindow: this.useTimewindow,
configDatasource: datasource,
configDatasourceIndex: index,
dataLoaded: (pageData, data1, datasourceIndex, pageLink) => {
@ -626,22 +631,31 @@ export class WidgetSubscription implements IWidgetSubscription {
}
onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) {
if (this.type === widgetType.timeseries || this.type === widgetType.alarm) {
let doUpdate = false;
let isTimewindowTypeChanged = false;
if (this.useTimewindow) {
if (this.useDashboardTimewindow) {
if (this.type === widgetType.latest) {
if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) {
this.timezone = newDashboardTimewindow.timezone;
doUpdate = this.updateTsOffset();
}
}
if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow);
isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow);
this.timeWindowConfig = deepClone(newDashboardTimewindow);
this.update(isTimewindowTypeChanged);
doUpdate = true;
}
}
} else if (this.type === widgetType.latest) {
if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) {
this.timezone = newDashboardTimewindow.timezone;
if (this.updateTsOffset()) {
this.update();
}
doUpdate = this.updateTsOffset();
}
}
if (doUpdate) {
this.update(isTimewindowTypeChanged);
}
}
updateDataVisibility(index: number): void {
@ -660,6 +674,12 @@ export class WidgetSubscription implements IWidgetSubscription {
updateTimewindowConfig(newTimewindow: Timewindow): void {
if (!this.useDashboardTimewindow) {
if (this.type === widgetType.latest) {
if (newTimewindow && this.timezone !== newTimewindow.timezone) {
this.timezone = newTimewindow.timezone;
this.updateTsOffset();
}
}
const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newTimewindow);
this.timeWindowConfig = newTimewindow;
this.update(isTimewindowTypeChanged);
@ -874,11 +894,12 @@ export class WidgetSubscription implements IWidgetSubscription {
}
const datasource = this.configuredDatasources[datasourceIndex];
if (datasource) {
if (this.type === widgetType.timeseries && this.timeWindowConfig) {
if (this.useTimewindow && this.timeWindowConfig) {
this.updateRealtimeSubscription();
}
entityDataListener = {
subscriptionType: this.type,
useTimewindow: this.useTimewindow,
configDatasource: datasource,
configDatasourceIndex: datasourceIndex,
subscriptionTimewindow: this.subscriptionTimewindow,
@ -940,7 +961,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateDataTimewindow() {
if (!this.hasDataPageLink) {
if (this.type === widgetType.timeseries && this.timeWindowConfig) {
if (this.useTimewindow && this.timeWindowConfig) {
this.updateRealtimeSubscription();
if (this.comparisonEnabled) {
this.updateSubscriptionForComparison();
@ -952,11 +973,11 @@ export class WidgetSubscription implements IWidgetSubscription {
private dataSubscribe() {
this.updateDataTimewindow();
if (!this.hasDataPageLink) {
if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) {
if (this.useTimewindow && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated();
}
const forceUpdate = !this.datasources.length;
const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length;
const notifyDataLoaded = !this.entityDataListeners.filter((listener) => !!listener.subscription).length;
this.entityDataListeners.forEach((listener) => {
if (this.comparisonEnabled && listener.configDatasource.isAdditional) {
listener.subscriptionTimewindow = this.timewindowForComparison;

4
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -34,6 +34,7 @@ import { Datasource, DatasourceType, Widget, widgetType } from '@app/shared/mode
import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
@Injectable({
providedIn: 'root'
@ -215,6 +216,9 @@ export class DashboardUtilsService {
delete datasource.deviceAliasId;
}
});
if (widget.type === widgetType.latest) {
widget.config.timewindow = initModelFromDefaultTimewindow(widget.config.timewindow, true, this.timeService);
}
// Temp workaround
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
widget.typeAlias = 'basic_timeseries';

3
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -1062,7 +1062,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
dataKeys: config.alarmSource.dataKeys || []
};
}
const newWidget: Widget = {
let newWidget: Widget = {
isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias,
typeAlias: widgetTypeInfo.alias,
@ -1076,6 +1076,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
row: 0,
col: 0
};
newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget);
if (widgetTypeInfo.typeParameters.useCustomDatasources) {
this.addWidgetToDashboard(newWidget);
} else {

1
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html

@ -36,6 +36,7 @@
[dashboard]="data.dashboard"
[aliasController]="data.aliasController"
[widget]="data.widget"
[widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks"
formControlName="dataKey">

3
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts

@ -22,7 +22,7 @@ import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { DataKey, Widget } from '@shared/models/widget.models';
import { DataKey, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models';
@ -35,6 +35,7 @@ export interface DataKeyConfigDialogData {
dashboard: Dashboard;
aliasController: IAliasController;
widget: Widget;
widgetType: widgetType;
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;

69
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html

@ -62,6 +62,75 @@
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</mat-form-field>
</div>
<section *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries" fxLayout="column">
<mat-form-field style="padding-bottom: 46px;">
<mat-label translate>datakey.aggregation</mat-label>
<mat-select formControlName="aggregationType" style="min-width: 150px;">
<mat-option *ngFor="let aggregation of aggregations" [value]="aggregation">
{{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}
</mat-option>
</mat-select>
<mat-hint style="line-height: 15px;">
{{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }}
<section *ngIf="dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE">
{{ 'datakey.aggregation-type-hint-common' | translate }}
</section>
</mat-hint>
</mat-form-field>
<fieldset *ngIf="dataKeyFormGroup.get('aggregationType').value && dataKeyFormGroup.get('aggregationType').value !== aggregationTypes.NONE" class="fields-group fields-group-slider">
<legend class="group-title" translate>datakey.delta-calculation</legend>
<mat-expansion-panel class="tb-settings" [expanded]="dataKeyFormGroup.get('comparisonEnabled').value" [disabled]="!dataKeyFormGroup.get('comparisonEnabled').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title fxLayout="column">
<mat-slide-toggle formControlName="comparisonEnabled" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'datakey.enable-delta-calculation' | translate }}
</mat-slide-toggle>
<mat-hint class="tb-hint" style="line-height: 15px; padding-left: 45px;">{{ 'datakey.enable-delta-calculation-hint' | translate }}</mat-hint>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<section fxLayout="column" *ngIf="dataKeyFormGroup.get('comparisonEnabled').value">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.chart.time-for-comparison</mat-label>
<mat-select formControlName="timeForComparison">
<mat-option [value]="'previousInterval'">
{{ 'widgets.chart.time-for-comparison-previous-interval' | translate }}
</mat-option>
<mat-option [value]="'days'">
{{ 'widgets.chart.time-for-comparison-days' | translate }}
</mat-option>
<mat-option [value]="'weeks'">
{{ 'widgets.chart.time-for-comparison-weeks' | translate }}
</mat-option>
<mat-option [value]="'months'">
{{ 'widgets.chart.time-for-comparison-months' | translate }}
</mat-option>
<mat-option [value]="'years'">
{{ 'widgets.chart.time-for-comparison-years' | translate }}
</mat-option>
<mat-option [value]="'customInterval'">
{{ 'widgets.chart.time-for-comparison-custom-interval' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="dataKeyFormGroup.get('timeForComparison').value === 'customInterval'" fxFlex class="mat-block">
<mat-label translate>widgets.chart.custom-interval-value</mat-label>
<input required matInput type="number" min="0" formControlName="comparisonCustomIntervalValue">
</mat-form-field>
<mat-form-field style="padding-bottom: 16px;">
<mat-label translate>datakey.delta-calculation-result</mat-label>
<mat-select formControlName="comparisonResultType" style="min-width: 150px;">
<mat-option *ngFor="let comparisonResultType of comparisonResults" [value]="comparisonResultType">
{{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
</ng-template>
</mat-expansion-panel>
</fieldset>
</section>
<section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function">
<span translate>datakey.data-generation-func</span>
<br/>

75
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss

@ -28,6 +28,36 @@
padding-left: 12px;
}
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
legend + * {
display: block;
margin-top: 16px;
}
&.fields-group-slider {
padding: 0;
legend {
margin-left: 16px;
}
> .tb-settings {
margin-top: 0;
padding: 0 16px 8px;
}
}
}
}
}
@ -42,5 +72,50 @@
}
}
}
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
padding: 0;
color: rgba(0, 0, 0, 0.87);
height: 140px;
&:hover {
background: none;
}
.mat-expansion-indicator {
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
.mat-slide {
margin: 8px 0;
}
.mat-slide-toggle-content {
white-space: normal;
}
}
}

137
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts

@ -18,7 +18,13 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DataKey, Widget } from '@shared/models/widget.models';
import {
ComparisonResultType, comparisonResultTypeTranslationMap,
DataKey,
dataKeyAggregationTypeHintTranslationMap,
Widget,
widgetType
} from '@shared/models/widget.models';
import {
ControlValueAccessor,
FormBuilder,
@ -43,6 +49,7 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co
import { WidgetService } from '@core/http/widget.service';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models';
@Component({
selector: 'tb-data-key-config',
@ -65,6 +72,22 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
dataKeyTypes = DataKeyType;
widgetTypes = widgetType;
aggregations = [AggregationType.NONE, ...Object.keys(AggregationType).filter(type => type !== AggregationType.NONE)];
aggregationTypes = AggregationType;
aggregationTypesTranslations = aggregationTranslations;
dataKeyAggregationTypeHintTranslations = dataKeyAggregationTypeHintTranslationMap;
comparisonResultTypes = ComparisonResultType;
comparisonResults = Object.keys(ComparisonResultType);
comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
entityAliasId: string;
@ -80,6 +103,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input()
widget: Widget;
@Input()
widgetType: widgetType;
@Input()
dataKeySettingsSchema: any;
@ -155,6 +181,11 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
}
this.dataKeyFormGroup = this.fb.group({
name: [null, []],
aggregationType: [null, []],
comparisonEnabled: [null, []],
timeForComparison: [null, [Validators.required]],
comparisonCustomIntervalValue: [null, [Validators.required, Validators.min(1000)]],
comparisonResultType: [null, [Validators.required]],
label: [null, [Validators.required]],
color: [null, [Validators.required]],
units: [null, []],
@ -164,6 +195,32 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
postFuncBody: [null, []]
});
this.dataKeyFormGroup.get('aggregationType').valueChanges.subscribe(
(aggType) => {
if (!this.dataKeyFormGroup.get('label').dirty) {
let newLabel = this.dataKeyFormGroup.get('name').value;
if (aggType !== AggregationType.NONE) {
const prefix = this.translate.instant(aggregationTranslations.get(aggType));
newLabel = prefix + ' ' + newLabel;
}
this.dataKeyFormGroup.get('label').patchValue(newLabel);
}
this.updateComparisonValidators();
}
);
this.dataKeyFormGroup.get('comparisonEnabled').valueChanges.subscribe(
() => {
this.updateComparisonValues();
}
);
this.dataKeyFormGroup.get('timeForComparison').valueChanges.subscribe(
() => {
this.updateComparisonValues();
}
);
this.dataKeyFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
@ -199,22 +256,86 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) {
this.modelValue.usePostProcessing = true;
}
if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) {
this.modelValue.aggregationType = AggregationType.NONE;
}
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false});
this.updateValidators();
if (this.displayAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings;
this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData
}, {emitEvent: false});
}
}
private updateValidators() {
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function &&
this.modelValue.type !== DataKeyType.count
? [Validators.required] : []);
this.modelValue.type !== DataKeyType.count
? [Validators.required] : []);
if (this.modelValue.type === DataKeyType.count) {
this.dataKeyFormGroup.get('name').disable({emitEvent: false});
} else {
this.dataKeyFormGroup.get('name').enable({emitEvent: false});
}
this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false});
if (this.displayAdvanced) {
this.dataKeySettingsData.model = this.modelValue.settings;
this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData
}, {emitEvent: false});
this.updateComparisonValidators();
}
private updateComparisonValues() {
const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value;
if (comparisonEnabled) {
const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value;
if (!timeForComparison) {
this.dataKeyFormGroup.get('timeForComparison').patchValue('previousInterval', {emitEvent: false});
} else if (timeForComparison === 'customInterval') {
const comparisonCustomIntervalValue = this.dataKeyFormGroup.get('comparisonCustomIntervalValue').value;
if (!comparisonCustomIntervalValue) {
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').patchValue(7200000, {emitEvent: false});
}
}
const comparisonResultType: ComparisonResultType = this.dataKeyFormGroup.get('comparisonResultType').value;
if (!comparisonResultType) {
this.dataKeyFormGroup.get('comparisonResultType').patchValue(ComparisonResultType.DELTA_ABSOLUTE, {emitEvent: false});
}
}
this.updateComparisonValidators();
}
private updateComparisonValidators() {
const aggregationType: AggregationType = this.dataKeyFormGroup.get('aggregationType').value;
if (aggregationType && aggregationType !== AggregationType.NONE) {
this.dataKeyFormGroup.get('comparisonEnabled').enable({emitEvent: false});
const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value;
if (comparisonEnabled) {
this.dataKeyFormGroup.get('timeForComparison').enable({emitEvent: false});
const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value;
if (timeForComparison) {
this.dataKeyFormGroup.get('comparisonResultType').enable({emitEvent: false});
if (timeForComparison === 'customInterval') {
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').enable({emitEvent: false});
} else {
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
} else {
this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
} else {
this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
} else {
this.dataKeyFormGroup.get('comparisonEnabled').disable({emitEvent: false});
this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false});
}
this.dataKeyFormGroup.get('comparisonEnabled').updateValueAndValidity({emitEvent: false});
this.dataKeyFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false});
this.dataKeyFormGroup.get('comparisonResultType').updateValueAndValidity({emitEvent: false});
this.dataKeyFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false});
}
private updateModel() {

7
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -58,12 +58,7 @@
{{key.label}}
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong *ngIf="datasourceType !== datasourceTypes.function && key.postFuncBody; else simpleChipLabel">f({{key.name}})</strong>
<ng-template #simpleChipLabel>
<strong>{{key.name}}</strong>
</ng-template>
</div>
<div class="tb-chip-label" [innerHTML]="displayDataKeyNameFn(key)"></div>
</div>
<button *ngIf="!disabled"
type="button"

7
ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss

@ -65,4 +65,11 @@
border-top: 0;
}
}
.tb-chip-label {
.tb-agg-func {
font-style: italic;
color: #0c959c;
}
}
}

36
ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts

@ -46,7 +46,7 @@ import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, DatasourceType, Widget, JsonSettingsSchema, widgetType } from '@shared/models/widget.models';
import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models';
@ -62,6 +62,8 @@ import {
import { deepClone } from '@core/utils';
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive';
import { Dashboard } from '@shared/models/dashboard.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { AggregationType } from '@shared/models/time/time.models';
@Component({
selector: 'tb-data-keys',
@ -173,6 +175,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private dialogs: DialogService,
private dialog: MatDialog,
private fb: FormBuilder,
private sanitizer: DomSanitizer,
public truncate: TruncatePipe) {
}
@ -424,6 +427,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
dashboard: this.dashboard,
aliasController: this.aliasController,
widget: this.widget,
widgetType: this.widgetType,
entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks
@ -446,6 +450,36 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
return key ? key.name : undefined;
}
displayDataKeyNameFn(key: DataKey): SafeHtml {
let keyName = key.name;
if (this.widgetType === widgetType.latest && key.type === DataKeyType.timeseries
&& key.aggregationType && key.aggregationType !== AggregationType.NONE) {
let aggFuncName: string;
switch (key.aggregationType) {
case AggregationType.MIN:
aggFuncName = 'MIN';
break;
case AggregationType.MAX:
aggFuncName = 'MAX';
break;
case AggregationType.AVG:
aggFuncName = 'AVG';
break;
case AggregationType.SUM:
aggFuncName = 'SUM';
break;
case AggregationType.COUNT:
aggFuncName = 'COUNT';
break;
}
keyName = `<span class="tb-agg-func">${aggFuncName}</span>(${keyName})`;
}
if (this.datasourceType !== DatasourceType.function && key.postFuncBody) {
keyName = `f(${keyName})`;
}
return this.sanitizer.bypassSecurityTrustHtml(`<strong>${keyName}</strong>`);
}
private fetchKeys(searchText?: string): Observable<Array<DataKey>> {
if (this.searchText !== searchText || this.latestSearchTextResult === null) {
this.searchText = searchText;

3
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -110,6 +110,7 @@ import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
import { AggregationType } from '@shared/models/time/time.models';
interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string;
@ -428,7 +429,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label);
dataKey.title = dataKey.label;
dataKey.def = 'def' + this.columns.length;
dataKey.sortable = !dataKey.usePostProcessing;
dataKey.sortable = !dataKey.usePostProcessing && (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE);
const keySettings: TableWidgetDataKeySettings = dataKey.settings;
if (dataKey.type === DataKeyType.entityField &&
!isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') {

4
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -17,7 +17,7 @@
// tslint:disable-next-line:no-reference
/// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" />
import { DataKey, Datasource, DatasourceData, JsonSettingsSchema } from '@shared/models/widget.models';
import { DataKey, Datasource, DatasourceData, FormattedData, JsonSettingsSchema } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { ComparisonDuration } from '@shared/models/time/time.models';
@ -25,7 +25,7 @@ export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph';
export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings;
export declare type TooltipValueFormatFunction = (value: any) => string;
export declare type TooltipValueFormatFunction = (value: any, latestData: FormattedData) => string;
export declare type TbFlotTicksFormatterFunction = (t: number, a?: TbFlotPlotAxis) => string;

49
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

@ -18,7 +18,7 @@
import { WidgetContext } from '@home/models/widget-component.models';
import {
createLabelFromDatasource,
deepClone,
deepClone, formattedDataFormDatasourceData,
insertVariable,
isDefined,
isDefinedAndNotNull,
@ -28,7 +28,14 @@ import {
isUndefined
} from '@app/core/utils';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DataKey, Datasource, DatasourceData, DatasourceType, widgetType } from '@app/shared/models/widget.models';
import {
DataKey,
Datasource,
DatasourceData,
DatasourceType,
FormattedData,
widgetType
} from '@app/shared/models/widget.models';
import {
ChartType,
TbFlotAxisOptions,
@ -88,6 +95,8 @@ export class TbFlot {
private latestDataThresholds: TbFlotThresholdMarking[];
private attributesThresholds: TbFlotThresholdMarking[];
private latestData: FormattedData[];
private labelPatternsSourcesSubscription: IWidgetSubscription;
private labelPatternsSourcesData: DatasourceData[];
@ -379,7 +388,7 @@ export class TbFlot {
let tooltipValueFormatFunction: TooltipValueFormatFunction = null;
if (this.settings.tooltipValueFormatter && this.settings.tooltipValueFormatter.length) {
try {
tooltipValueFormatFunction = new Function('value', this.settings.tooltipValueFormatter) as TooltipValueFormatFunction;
tooltipValueFormatFunction = new Function('value', 'latestData', this.settings.tooltipValueFormatter) as TooltipValueFormatFunction;
} catch (e) {
tooltipValueFormatFunction = null;
}
@ -392,7 +401,7 @@ export class TbFlot {
series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) {
try {
series.dataKey.tooltipValueFormatFunction = new Function('value',
series.dataKey.tooltipValueFormatFunction = new Function('value', 'latestData',
keySettings.tooltipValueFormatter) as TooltipValueFormatFunction;
} catch (e) {
series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
@ -546,6 +555,13 @@ export class TbFlot {
}
this.latestDataThresholds = this.thresholdsSourcesDataUpdated(allThresholds, this.subscription.latestData, true);
this.options.grid.markings = allThresholds.concat(this.latestDataThresholds);
if (this.subscription.latestData) {
this.latestData = formattedDataFormDatasourceData(this.subscription.latestData);
} else {
this.latestData = [];
}
} else if (this.chartType === 'pie') {
this.latestData = formattedDataFormDatasourceData(this.subscription.data);
}
this.checkMouseEvents();
@ -653,6 +669,7 @@ export class TbFlot {
this.updateData();
}
} else if (this.chartType === 'pie') {
this.latestData = formattedDataFormDatasourceData(this.subscription.data);
if (this.animatedPie) {
this.nextPieDataAnimation(true);
} else {
@ -683,6 +700,11 @@ export class TbFlot {
this.plot.getOptions().grid.markings = this.options.grid.markings;
this.updateData();
}
if (this.subscription.latestData) {
this.latestData = formattedDataFormDatasourceData(this.subscription.latestData);
} else {
this.latestData = [];
}
}
} else if (this.isMouseInteraction && this.plot) {
this.latestUpdateTimeoutHandle = setTimeout(this.latestDataUpdate.bind(this), 30);
@ -690,6 +712,14 @@ export class TbFlot {
}
}
private latestDataByDataIndex(index: number): FormattedData {
if (this.latestData[index]) {
return this.latestData[index];
} else {
return {} as FormattedData;
}
}
private scalingPieRadius() {
let scalingLine;
this.ctx.width > this.ctx.height ? scalingLine = this.ctx.height : scalingLine = this.ctx.width;
@ -1004,7 +1034,8 @@ export class TbFlot {
private seriesInfoDiv(label: string, color: string, value: any,
units: string, trackDecimals: number, active: boolean,
percent: number, valueFormatFunction: TooltipValueFormatFunction): JQuery<HTMLElement> {
percent: number, seriesIndex: number,
valueFormatFunction: TooltipValueFormatFunction): JQuery<HTMLElement> {
const divElement = $('<div></div>');
divElement.css({
display: 'flex',
@ -1034,7 +1065,7 @@ export class TbFlot {
divElement.append(labelSpan);
let valueContent: string;
if (valueFormatFunction) {
valueContent = valueFormatFunction(value);
valueContent = valueFormatFunction(value, this.latestDataByDataIndex(seriesIndex));
} else {
valueContent = this.ctx.utils.formatValue(value, trackDecimals, units);
}
@ -1059,7 +1090,8 @@ export class TbFlot {
const units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : this.trackUnits;
const decimals = isDefinedAndNotNull(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : this.trackDecimals;
const divElement = this.seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color,
seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex, null, seriesHoverInfo.tooltipValueFormatFunction);
seriesHoverInfo.value, units, decimals, seriesHoverInfo.index === seriesIndex, null, seriesHoverInfo.index,
seriesHoverInfo.tooltipValueFormatFunction);
return divElement.prop('outerHTML');
}
@ -1086,7 +1118,8 @@ export class TbFlot {
const units = item.series.dataKey.units && item.series.dataKey.units.length ? item.series.dataKey.units : this.trackUnits;
const decimals = isDefinedAndNotNull(item.series.dataKey.decimals) ? item.series.dataKey.decimals : this.trackDecimals;
const divElement = this.seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color,
item.datapoint[1][0][1], units, decimals, true, item.series.percent, item.series.dataKey.tooltipValueFormatFunction);
item.datapoint[1][0][1], units, decimals, true, item.series.percent, 0,
item.series.dataKey.tooltipValueFormatFunction);
return divElement.prop('outerHTML');
}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-key-settings.component.html

@ -123,7 +123,7 @@
<tb-js-func
formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['value']"
[functionArgs]="['value', 'latestData']"
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html

@ -96,7 +96,7 @@
<tb-js-func
formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables"
[functionArgs]="['value']"
[functionArgs]="['value', 'latestData']"
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func>

5
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -18,7 +18,7 @@
<mat-tab-group class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
<div *ngIf="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.alarm" fxFlex="100"
<div *ngIf="displayTimewindowConfig()" fxFlex="100"
fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs>
<mat-checkbox formControlName="useDashboardTimewindow">
@ -34,6 +34,9 @@
style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow asButton="true"
isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow()"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
fxFlex formControlName="timewindow"></tb-timewindow>
</section>

39
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
DataKey,
Datasource,
Datasource, datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation,
DatasourceType,
datasourceTypeTranslationMap,
defaultLegendConfig,
@ -42,7 +42,7 @@ import {
Validators
} from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, isDefined, isObject, isUndefined } from '@app/core/utils';
import { deepClone, isDefined, isObject } from '@app/core/utils';
import {
alarmFields,
AlarmSearchStatus,
@ -51,7 +51,7 @@ import {
alarmSeverityTranslations
} from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias, EntityAliases } from '@shared/models/alias.models';
import { EntityAlias } from '@shared/models/alias.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core';
@ -67,13 +67,14 @@ import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { WidgetActionsData } from './action/manage-widget-actions.component.models';
import { Dashboard, DashboardState } from '@shared/models/dashboard.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models';
import { Filter, Filters } from '@shared/models/query/query.models';
import { Filter } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { AggregationType } from '@shared/models/time/time.models';
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -334,10 +335,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null));
this.dataSettings.addControl('displayTimewindow', this.fb.control(null));
this.dataSettings.addControl('timewindow', this.fb.control(null));
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true));
this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true}));
this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true}));
this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => {
if (value) {
this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
@ -467,7 +468,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
},
{emitEvent: false}
);
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true;
this.dataSettings.patchValue(
@ -733,6 +734,24 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
!!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length);
}
public displayTimewindowConfig(): boolean {
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) {
return true;
} else if (this.widgetType === widgetType.latest) {
const datasources = this.dataSettings.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
}
public onlyHistoryTimewindow(): boolean {
if (this.widgetType === widgetType.latest) {
const datasources = this.dataSettings.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
} else {
return false;
}
}
public onDatasourceDrop(event: CdkDragDrop<string[]>) {
const datasourcesFormArray = this.datasourcesFormArray();
const datasourceForm = datasourcesFormArray.at(event.previousIndex);

3
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -46,6 +46,9 @@
</span>
<tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}"
quickIntervalOnly="{{widget.onlyQuickInterval}}"
historyOnly="{{widget.onlyHistoryTimewindow}}"
alwaysDisplayTypePrefix
timezone="true"
[isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow"

34
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -15,16 +15,24 @@
///
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import { FormattedData, Widget, WidgetPosition, widgetType } from '@app/shared/models/widget.models';
import {
Datasource,
datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation,
FormattedData,
Widget,
WidgetPosition,
widgetType
} from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { Timewindow } from '@shared/models/time/time.models';
import { AggregationType, Timewindow } from '@shared/models/time/time.models';
import { Observable, of, Subject } from 'rxjs';
import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
export interface WidgetsData {
widgets: Array<Widget>;
@ -325,6 +333,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
hasAggregation: boolean;
onlyQuickInterval: boolean;
onlyHistoryTimewindow: boolean;
style: {[klass: string]: any};
showWidgetTitlePanel: boolean;
@ -420,12 +432,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;
this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true;
this.hasTimewindow = (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) ?
let canHaveTimewindow = false;
let onlyQuickInterval = false;
let onlyHistoryTimewindow = false;
if (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) {
canHaveTimewindow = true;
} else if (this.widget.type === widgetType.latest) {
canHaveTimewindow = datasourcesHasAggregation(this.widget.config.datasources);
onlyQuickInterval = canHaveTimewindow;
if (canHaveTimewindow) {
onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(this.widget.config.datasources);
}
}
this.hasTimewindow = canHaveTimewindow ?
(isDefined(this.widget.config.useDashboardTimewindow) ?
(!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow)
|| this.widget.config.displayTimewindow)) : false)
: false;
this.onlyQuickInterval = onlyQuickInterval;
this.onlyHistoryTimewindow = onlyHistoryTimewindow;
this.hasAggregation = this.widget.type === widgetType.timeseries;
this.style = {

25
ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss

@ -0,0 +1,25 @@
/**
* Copyright © 2016-2022 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 '../../../../scss/constants';
:host {
min-width: 355px;
@media #{$mat-xs} {
min-width: 0;
width: 100%;
}
}

1
ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts

@ -21,6 +21,7 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode
@Component({
selector: 'tb-quick-time-interval',
templateUrl: './quick-time-interval.component.html',
styleUrls: ['./quick-time-interval.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

63
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -29,32 +29,57 @@
</section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;">
<mat-radio-group formControlName="realtimeType">
<mat-radio-group *ngIf="!quickIntervalOnly" [fxShow]="isEdit || (!timewindow.hideLastInterval && !timewindow.hideQuickInterval)"
formControlName="realtimeType">
<mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideLastInterval"
(ngModelChange)="onHideLastIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.last</span>
<tb-timeinterval
formControlName="timewindowMs"
predefinedName="timewindow.last"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL"
style="padding-top: 8px;"></tb-timeinterval>
</section>
</section>
</mat-radio-button>
<mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
<section fxLayout="row">
<section *ngIf="isEdit" fxLayout="column" style="padding-right: 8px;">
<label class="tb-small hide-label" translate>timewindow.hide</label>
<mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideQuickInterval"
(ngModelChange)="onHideQuickIntervalChanged()"></mat-checkbox>
</section>
<section fxLayout="column">
<span translate>timewindow.interval</span>
<tb-quick-time-interval
formControlName="quickInterval"
onlyCurrentInterval="true"
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px"></tb-quick-time-interval>
</section>
</section>
</mat-radio-button>
</mat-radio-group>
<tb-timeinterval *ngIf="!isEdit && !timewindow.hideLastInterval && timewindow.hideQuickInterval"
formControlName="timewindowMs"
predefinedName="timewindow.last"
required
style="padding-top: 8px;"></tb-timeinterval>
<tb-quick-time-interval *ngIf="quickIntervalOnly || !isEdit && timewindow.hideLastInterval && !timewindow.hideQuickInterval"
formControlName="quickInterval"
onlyCurrentInterval="true"
required
style="padding-top: 8px"></tb-quick-time-interval>
</div>
</section>
</section>

53
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -36,6 +36,7 @@ export const TIMEWINDOW_PANEL_DATA = new InjectionToken<any>('TimewindowPanelDat
export interface TimewindowPanelData {
historyOnly: boolean;
quickIntervalOnly: boolean;
timewindow: Timewindow;
aggregation: boolean;
timezone: boolean;
@ -51,6 +52,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
historyOnly = false;
quickIntervalOnly = false;
aggregation = false;
timezone = false;
@ -83,6 +86,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
public viewContainerRef: ViewContainerRef) {
super(store);
this.historyOnly = data.historyOnly;
this.quickIntervalOnly = data.quickIntervalOnly;
this.timewindow = data.timewindow;
this.aggregation = data.aggregation;
this.timezone = data.timezone;
@ -91,6 +95,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
ngOnInit(): void {
const hideInterval = this.timewindow.hideInterval || false;
const hideLastInterval = this.timewindow.hideLastInterval || false;
const hideQuickInterval = this.timewindow.hideQuickInterval || false;
const hideAggregation = this.timewindow.hideAggregation || false;
const hideAggInterval = this.timewindow.hideAggInterval || false;
const hideTimezone = this.timewindow.hideTimezone || false;
@ -103,10 +109,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
disabled: hideInterval
}),
timewindowMs: [
this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
? this.timewindow.realtime.timewindowMs : null
],
timewindowMs: this.fb.control({
value: this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
? this.timewindow.realtime.timewindowMs : null,
disabled: hideInterval || hideLastInterval
}),
interval: [
this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined'
? this.timewindow.realtime.interval : null
@ -114,7 +121,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
quickInterval: this.fb.control({
value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined'
? this.timewindow.realtime.quickInterval : null,
disabled: hideInterval
disabled: hideInterval || hideQuickInterval
})
}
),
@ -289,8 +296,40 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false});
this.timewindowForm.get('history.quickInterval').enable({emitEvent: false});
this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false});
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
if (!this.timewindow.hideLastInterval) {
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
}
if (!this.timewindow.hideQuickInterval) {
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
}
}
this.timewindowForm.markAsDirty();
}
onHideLastIntervalChanged() {
if (this.timewindow.hideLastInterval) {
this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false});
if (!this.timewindow.hideQuickInterval) {
this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.INTERVAL);
}
} else {
if (!this.timewindow.hideInterval) {
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false});
}
}
this.timewindowForm.markAsDirty();
}
onHideQuickIntervalChanged() {
if (this.timewindow.hideQuickInterval) {
this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false});
if (!this.timewindow.hideLastInterval) {
this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.LAST_INTERVAL);
}
} else {
if (!this.timewindow.hideInterval) {
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false});
}
}
this.timewindowForm.markAsDirty();
}

54
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -75,13 +75,41 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
@Input()
set historyOnly(val) {
this.historyOnlyValue = coerceBooleanProperty(val);
const newHistoryOnlyValue = coerceBooleanProperty(val);
if (this.historyOnlyValue !== newHistoryOnlyValue) {
this.historyOnlyValue = newHistoryOnlyValue;
if (this.onHistoryOnlyChanged()) {
this.notifyChanged();
}
}
}
get historyOnly() {
return this.historyOnlyValue;
}
alwaysDisplayTypePrefixValue = false;
@Input()
set alwaysDisplayTypePrefix(val) {
this.alwaysDisplayTypePrefixValue = coerceBooleanProperty(val);
}
get alwaysDisplayTypePrefix() {
return this.alwaysDisplayTypePrefixValue;
}
quickIntervalOnlyValue = false;
@Input()
set quickIntervalOnly(val) {
this.quickIntervalOnlyValue = coerceBooleanProperty(val);
}
get quickIntervalOnly() {
return this.quickIntervalOnlyValue;
}
aggregationValue = false;
@Input()
@ -240,6 +268,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
{
timewindow: deepClone(this.innerValue),
historyOnly: this.historyOnly,
quickIntervalOnly: this.quickIntervalOnly,
aggregation: this.aggregation,
timezone: this.timezone,
isEdit: this.isEdit
@ -265,6 +294,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
return Injector.create({parent: this.viewContainerRef.injector, providers});
}
private onHistoryOnlyChanged(): boolean {
if (this.historyOnlyValue && this.innerValue) {
if (this.innerValue.selectedTab !== TimewindowType.HISTORY) {
this.innerValue.selectedTab = TimewindowType.HISTORY;
this.updateDisplayValue();
return true;
}
}
return false;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@ -278,9 +318,15 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
}
writeValue(obj: Timewindow): void {
this.innerValue = initModelFromDefaultTimewindow(obj, this.timeService);
this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.timeService);
this.timewindowDisabled = this.isTimewindowDisabled();
this.updateDisplayValue();
if (this.onHistoryOnlyChanged()) {
setTimeout(() => {
this.notifyChanged();
});
} else {
this.updateDisplayValue();
}
}
notifyChanged() {
@ -297,7 +343,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
}
} else {
this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : '';
this.innerValue.displayValue = (!this.historyOnly || this.alwaysDisplayTypePrefix) ? (this.translate.instant('timewindow.history') + ' - ') : '';
if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs);

8
ui-ngx/src/app/shared/models/query/query.models.ts

@ -738,7 +738,6 @@ export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPag
}
export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1);
export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1024);
export interface EntityCountQuery {
entityFilter: EntityFilter;
@ -761,12 +760,19 @@ export interface AlarmDataQuery extends AbstractDataQuery<AlarmDataPageLink> {
export interface TsValue {
ts: number;
value: string;
count?: number;
}
export interface ComparisonTsValue {
current?: TsValue;
previous?: TsValue;
}
export interface EntityData {
entityId: EntityId;
latest: {[entityKeyType: string]: {[key: string]: TsValue}};
timeseries: {[key: string]: Array<TsValue>};
aggLatest?: {[id: number]: ComparisonTsValue};
}
export interface AlarmData extends AlarmInfo {

127
ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts

@ -173,12 +173,35 @@ export interface TimeSeriesCmd {
fetchLatestPreviousPoint?: boolean;
}
export interface AggKey {
id: number;
key: string;
agg: AggregationType;
previousStartTs?: number;
previousEndTs?: number;
previousValueOnly?: boolean;
}
export interface AggEntityHistoryCmd {
keys: Array<AggKey>;
startTs: number;
endTs: number;
}
export interface AggTimeSeriesCmd {
keys: Array<AggKey>;
startTs: number;
timeWindow: number;
}
export class EntityDataCmd implements WebsocketCmd {
cmdId: number;
query?: EntityDataQuery;
historyCmd?: EntityHistoryCmd;
latestCmd?: LatestValueCmd;
tsCmd?: TimeSeriesCmd;
aggHistoryCmd?: AggEntityHistoryCmd;
aggTsCmd?: AggTimeSeriesCmd;
public isEmpty(): boolean {
return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd;
@ -212,15 +235,6 @@ export class AlarmDataUnsubscribeCmd implements WebsocketCmd {
}
export class TelemetryPluginCmdsWrapper {
attrSubCmds: Array<AttributesSubscriptionCmd>;
tsSubCmds: Array<TimeseriesSubscriptionCmd>;
historyCmds: Array<GetHistoryCmd>;
entityDataCmds: Array<EntityDataCmd>;
entityDataUnsubscribeCmds: Array<EntityDataUnsubscribeCmd>;
alarmDataCmds: Array<AlarmDataCmd>;
alarmDataUnsubscribeCmds: Array<AlarmDataUnsubscribeCmd>;
entityCountCmds: Array<EntityCountCmd>;
entityCountUnsubscribeCmds: Array<EntityCountUnsubscribeCmd>;
constructor() {
this.attrSubCmds = [];
@ -233,6 +247,24 @@ export class TelemetryPluginCmdsWrapper {
this.entityCountCmds = [];
this.entityCountUnsubscribeCmds = [];
}
attrSubCmds: Array<AttributesSubscriptionCmd>;
tsSubCmds: Array<TimeseriesSubscriptionCmd>;
historyCmds: Array<GetHistoryCmd>;
entityDataCmds: Array<EntityDataCmd>;
entityDataUnsubscribeCmds: Array<EntityDataUnsubscribeCmd>;
alarmDataCmds: Array<AlarmDataCmd>;
alarmDataUnsubscribeCmds: Array<AlarmDataUnsubscribeCmd>;
entityCountCmds: Array<EntityCountCmd>;
entityCountUnsubscribeCmds: Array<EntityCountUnsubscribeCmd>;
private static popCmds<T>(cmds: Array<T>, leftCount: number): Array<T> {
const toPublish = Math.min(cmds.length, leftCount);
if (toPublish > 0) {
return cmds.splice(0, toPublish);
} else {
return [];
}
}
public hasCommands(): boolean {
return this.tsSubCmds.length > 0 ||
@ -261,38 +293,33 @@ export class TelemetryPluginCmdsWrapper {
public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper {
const preparedWrapper = new TelemetryPluginCmdsWrapper();
let leftCount = maxCommands;
preparedWrapper.tsSubCmds = this.popCmds(this.tsSubCmds, leftCount);
preparedWrapper.tsSubCmds = TelemetryPluginCmdsWrapper.popCmds(this.tsSubCmds, leftCount);
leftCount -= preparedWrapper.tsSubCmds.length;
preparedWrapper.historyCmds = this.popCmds(this.historyCmds, leftCount);
preparedWrapper.historyCmds = TelemetryPluginCmdsWrapper.popCmds(this.historyCmds, leftCount);
leftCount -= preparedWrapper.historyCmds.length;
preparedWrapper.attrSubCmds = this.popCmds(this.attrSubCmds, leftCount);
preparedWrapper.attrSubCmds = TelemetryPluginCmdsWrapper.popCmds(this.attrSubCmds, leftCount);
leftCount -= preparedWrapper.attrSubCmds.length;
preparedWrapper.entityDataCmds = this.popCmds(this.entityDataCmds, leftCount);
preparedWrapper.entityDataCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityDataCmds, leftCount);
leftCount -= preparedWrapper.entityDataCmds.length;
preparedWrapper.entityDataUnsubscribeCmds = this.popCmds(this.entityDataUnsubscribeCmds, leftCount);
preparedWrapper.entityDataUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityDataUnsubscribeCmds, leftCount);
leftCount -= preparedWrapper.entityDataUnsubscribeCmds.length;
preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount);
preparedWrapper.alarmDataCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmDataCmds, leftCount);
leftCount -= preparedWrapper.alarmDataCmds.length;
preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount);
preparedWrapper.alarmDataUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmDataUnsubscribeCmds, leftCount);
leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length;
preparedWrapper.entityCountCmds = this.popCmds(this.entityCountCmds, leftCount);
preparedWrapper.entityCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountCmds, leftCount);
leftCount -= preparedWrapper.entityCountCmds.length;
preparedWrapper.entityCountUnsubscribeCmds = this.popCmds(this.entityCountUnsubscribeCmds, leftCount);
preparedWrapper.entityCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountUnsubscribeCmds, leftCount);
return preparedWrapper;
}
private popCmds<T>(cmds: Array<T>, leftCount: number): Array<T> {
const toPublish = Math.min(cmds.length, leftCount);
if (toPublish > 0) {
return cmds.splice(0, toPublish);
} else {
return [];
}
}
}
export interface SubscriptionData {
[key: string]: [number, any][];
[key: string]: [number, any, number?][];
}
export interface IndexedSubscriptionData {
[id: number]: [number, any, number?][];
}
export interface SubscriptionDataHolder {
@ -434,16 +461,7 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
super(msg);
}
public prepareData(tsOffset: number) {
if (this.data) {
this.processEntityData(this.data.data, tsOffset);
}
if (this.update) {
this.processEntityData(this.update, tsOffset);
}
}
private processEntityData(data: Array<EntityData>, tsOffset: number) {
private static processEntityData(data: Array<EntityData>, tsOffset: number) {
for (const entityData of data) {
if (entityData.timeseries) {
for (const key of Object.keys(entityData.timeseries)) {
@ -471,28 +489,28 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
}
}
}
public prepareData(tsOffset: number) {
if (this.data) {
EntityDataUpdate.processEntityData(this.data.data, tsOffset);
}
if (this.update) {
EntityDataUpdate.processEntityData(this.update, tsOffset);
}
}
}
export class AlarmDataUpdate extends DataUpdate<AlarmData> {
allowedEntities: number;
totalEntities: number;
constructor(msg: AlarmDataUpdateMsg) {
super(msg);
this.allowedEntities = msg.allowedEntities;
this.totalEntities = msg.totalEntities;
}
allowedEntities: number;
totalEntities: number;
public prepareData(tsOffset: number) {
if (this.data) {
this.processAlarmData(this.data.data, tsOffset);
}
if (this.update) {
this.processAlarmData(this.update, tsOffset);
}
}
private processAlarmData(data: Array<AlarmData>, tsOffset: number) {
private static processAlarmData(data: Array<AlarmData>, tsOffset: number) {
for (const alarmData of data) {
alarmData.createdTime += tsOffset;
if (alarmData.ackTs) {
@ -524,6 +542,15 @@ export class AlarmDataUpdate extends DataUpdate<AlarmData> {
}
}
}
public prepareData(tsOffset: number) {
if (this.data) {
AlarmDataUpdate.processAlarmData(this.data.data, tsOffset);
}
if (this.update) {
AlarmDataUpdate.processAlarmData(this.update, tsOffset);
}
}
}
export class EntityCountUpdate extends CmdUpdate {

15
ui-ngx/src/app/shared/models/time/time.models.ts

@ -96,6 +96,8 @@ export interface Timewindow {
displayValue?: string;
displayTimezoneAbbr?: string;
hideInterval?: boolean;
hideQuickInterval?: boolean;
hideLastInterval?: boolean;
hideAggregation?: boolean;
hideAggInterval?: boolean;
hideTimezone?: boolean;
@ -188,6 +190,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
return {
displayValue: '',
hideInterval: false,
hideLastInterval: false,
hideQuickInterval: false,
hideAggregation: false,
hideAggInterval: false,
hideTimezone: false,
@ -223,10 +227,12 @@ function getTimewindowType(timewindow: Timewindow): TimewindowType {
}
}
export function initModelFromDefaultTimewindow(value: Timewindow, timeService: TimeService): Timewindow {
export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow {
const model = defaultTimewindow(timeService);
if (value) {
model.hideInterval = value.hideInterval;
model.hideLastInterval = value.hideLastInterval;
model.hideQuickInterval = value.hideQuickInterval;
model.hideAggregation = value.hideAggregation;
model.hideAggInterval = value.hideAggInterval;
model.hideTimezone = value.hideTimezone;
@ -281,6 +287,9 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
}
model.timezone = value.timezone;
}
if (quickIntervalOnly) {
model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
}
return model;
}
@ -304,6 +313,8 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
}
return {
hideInterval: timewindow.hideInterval || false,
hideLastInterval: timewindow.hideLastInterval || false,
hideQuickInterval: timewindow.hideQuickInterval || false,
hideAggregation: timewindow.hideAggregation || false,
hideAggInterval: timewindow.hideAggInterval || false,
hideTimezone: timewindow.hideTimezone || false,
@ -694,6 +705,8 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti
export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
const cloned: Timewindow = {};
cloned.hideInterval = timewindow.hideInterval || false;
cloned.hideLastInterval = timewindow.hideLastInterval || false;
cloned.hideQuickInterval = timewindow.hideQuickInterval || false;
cloned.hideAggregation = timewindow.hideAggregation || false;
cloned.hideAggInterval = timewindow.hideAggInterval || false;
cloned.hideTimezone = timewindow.hideTimezone || false;

64
ui-ngx/src/app/shared/models/widget.models.ts

@ -17,7 +17,7 @@
import { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { Timewindow } from '@shared/models/time/time.models';
import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models';
import { DataKeyType } from './telemetry/telemetry.models';
@ -259,8 +259,27 @@ export function defaultLegendConfig(wType: widgetType): LegendConfig {
};
}
export enum ComparisonResultType {
PREVIOUS_VALUE = 'PREVIOUS_VALUE',
DELTA_ABSOLUTE = 'DELTA_ABSOLUTE',
DELTA_PERCENT = 'DELTA_PERCENT'
}
export const comparisonResultTypeTranslationMap = new Map<ComparisonResultType, string>(
[
[ComparisonResultType.PREVIOUS_VALUE, 'datakey.delta-calculation-result-previous-value'],
[ComparisonResultType.DELTA_ABSOLUTE, 'datakey.delta-calculation-result-delta-absolute'],
[ComparisonResultType.DELTA_PERCENT, 'datakey.delta-calculation-result-delta-percent']
]
);
export interface KeyInfo {
name: string;
aggregationType?: AggregationType;
comparisonEnabled?: boolean;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
comparisonResultType?: ComparisonResultType;
label?: string;
color?: string;
funcBody?: string;
@ -269,6 +288,18 @@ export interface KeyInfo {
decimals?: number;
}
export const dataKeyAggregationTypeHintTranslationMap = new Map<AggregationType, string>(
[
[AggregationType.MIN, 'datakey.aggregation-type-min-hint'],
[AggregationType.MAX, 'datakey.aggregation-type-max-hint'],
[AggregationType.AVG, 'datakey.aggregation-type-avg-hint'],
[AggregationType.SUM, 'datakey.aggregation-type-sum-hint'],
[AggregationType.COUNT, 'datakey.aggregation-type-count-hint'],
[AggregationType.NONE, 'datakey.aggregation-type-none-hint'],
]
);
export interface DataKey extends KeyInfo {
type: DataKeyType;
pattern?: string;
@ -322,6 +353,37 @@ export interface Datasource {
[key: string]: any;
}
export function datasourcesHasAggregation(datasources?: Array<Datasource>): boolean {
if (datasources) {
const foundDatasource = datasources.find(datasource => {
const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries &&
key.aggregationType && key.aggregationType !== AggregationType.NONE);
return !!found;
});
if (foundDatasource) {
return true;
}
}
return false;
}
export function datasourcesHasOnlyComparisonAggregation(datasources?: Array<Datasource>): boolean {
if (!datasourcesHasAggregation(datasources)) {
return false;
}
if (datasources) {
const foundDatasource = datasources.find(datasource => {
const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries &&
key.aggregationType && key.aggregationType !== AggregationType.NONE && !key.comparisonEnabled);
return !!found;
});
if (foundDatasource) {
return false;
}
}
return true;
}
export interface FormattedData {
$datasource: Datasource;
entityName: string;

17
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1039,7 +1039,22 @@
"value-description": "the current value;",
"prev-value-description": "result of the previous function call;",
"time-prev-description": "timestamp of the previous value;",
"prev-orig-value-description": "original previous value;"
"prev-orig-value-description": "original previous value;",
"aggregation": "Aggregation",
"aggregation-type-hint-common": "For performance reasons, the aggregated values calculation is available only for fixed time intervals like \"current day\", \"current month\", etc, and is not available for sliding window intervals like 'last 30 minutes' or 'last 24 hours'.",
"aggregation-type-none-hint": "Take latest value.",
"aggregation-type-min-hint": "Find minimum value among data points within a selected time window.",
"aggregation-type-max-hint": "Find maximum value among data points within a selected time window.",
"aggregation-type-avg-hint": "Calculate an average value among data points within a selected time window.",
"aggregation-type-sum-hint": "Sum all values of the data points within a selected time window.",
"aggregation-type-count-hint": "Total number of the data points within a selected time window.",
"delta-calculation": "Delta calculation",
"enable-delta-calculation": "Enable delta calculation",
"enable-delta-calculation-hint": "When enabled, the data key value is calculated based on the aggregated values for a selected time window and a specified comparison period. For performance reasons, the delta calculation is available only for history time windows and not for real-time values. For example, you may calculate the delta between the energy consumption for yesterday compared to the energy consumption for the day before yesterday.",
"delta-calculation-result": "Delta calculation result",
"delta-calculation-result-previous-value": "Previous value",
"delta-calculation-result-delta-absolute": "Delta (absolute)",
"delta-calculation-result-delta-percent": "Delta (percent)"
},
"datasource": {
"type": "Datasource type",

Loading…
Cancel
Save