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.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; 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.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -30,13 +29,13 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardThreadFactory; 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.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmDataQuery; 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.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey; 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.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; 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.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
@ -68,12 +70,12 @@ import javax.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -83,6 +85,7 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j @Slf4j
@TbCoreComponent @TbCoreComponent
@Service @Service
@ -131,6 +134,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private int maxEntitiesPerAlarmSubscription; private int maxEntitiesPerAlarmSubscription;
@Value("${server.ws.dynamic_page_link.max_alarm_queries_per_refresh_interval:10}") @Value("${server.ws.dynamic_page_link.max_alarm_queries_per_refresh_interval:10}")
private int maxAlarmQueriesPerRefreshInterval; private int maxAlarmQueriesPerRefreshInterval;
@Value("${ui.dashboard.max_datapoints_limit:50000}")
private int maxDatapointLimit;
private ExecutorService wsCallBackExecutor; private ExecutorService wsCallBackExecutor;
private boolean tsInSqlDB; private boolean tsInSqlDB;
@ -165,7 +170,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
if (ctx != null) { if (ctx != null) {
log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd); 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(); ctx.clearEntitySubscriptions();
} }
} else { } else {
@ -173,6 +178,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
ctx = createSubCtx(session, cmd); ctx = createSubCtx(session, cmd);
} }
ctx.setCurrentCmd(cmd); ctx.setCurrentCmd(cmd);
// Fetch entity list using entity data query
if (cmd.getQuery() != null) { if (cmd.getQuery() != null) {
if (ctx.getQuery() == null) { if (ctx.getQuery() == null) {
log.debug("[{}][{}] Initializing data using query: {}", session.getSessionId(), cmd.getCmdId(), cmd.getQuery()); log.debug("[{}][{}] Initializing data using query: {}", session.getSessionId(), cmd.getCmdId(), cmd.getQuery());
@ -204,43 +211,143 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
finalCtx.setRefreshTask(task); finalCtx.setRefreshTask(task);
} }
} }
ListenableFuture<TbEntityDataSubCtx> historyFuture;
if (cmd.getHistoryCmd() != null) { try {
log.trace("[{}][{}] Going to process history command: {}", session.getSessionId(), cmd.getCmdId(), cmd.getHistoryCmd()); List<ListenableFuture<?>> cmdFutures = new ArrayList<>();
try { if (cmd.getAggHistoryCmd() != null) {
historyFuture = handleHistoryCmd(ctx, cmd.getHistoryCmd()); cmdFutures.add(handleAggHistoryCmd(ctx, cmd.getAggHistoryCmd()));
} catch (RuntimeException e) {
handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd);
return;
} }
} else { if (cmd.getAggTsCmd() != null) {
historyFuture = Futures.immediateFuture(ctx); 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 { try {
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) { Map<String, Long> lastTsMap = new HashMap<>();
if (cmd.getLatestCmd() != null) { lastTsEntityMap.put(entityData, lastTsMap);
handleLatestCmd(theCtx, cmd.getLatestCmd());
} List<ReadTsKvQueryResult> queryResults = future.get();
if (cmd.getTsCmd() != null) { if (queryResults != null) {
handleTimeSeriesCmd(theCtx, cmd.getTsCmd()); 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) { // Populate with empty values if no data found.
handleWsCmdRuntimeException(theCtx.getSessionId(), e, cmd); 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();
} }
return ctx;
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process command", session.getSessionId(), cmd.getCmdId());
}
}, wsCallBackExecutor); }, wsCallBackExecutor);
} }
@ -416,36 +523,58 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
} }
private ListenableFuture<TbEntityDataSubCtx> handleGetTsCmd(TbEntityDataSubCtx ctx, GetTsCmd cmd, boolean subscribe) { private ListenableFuture<TbEntityDataSubCtx> handleGetTsCmd(TbEntityDataSubCtx ctx, GetTsCmd cmd, boolean subscribe) {
Map<Integer, String> queriesKeys = new ConcurrentHashMap<>();
List<String> keys = cmd.getKeys(); List<String> keys = cmd.getKeys();
List<ReadTsKvQuery> finalTsKvQueryList; List<ReadTsKvQuery> finalTsKvQueryList;
List<ReadTsKvQuery> tsKvQueryList = cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( List<ReadTsKvQuery> tsKvQueryList = keys.stream().map(key -> {
key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg() var query = new BaseReadTsKvQuery(
)).collect(Collectors.toList()); 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()) { if (cmd.isFetchLatestPreviousPoint()) {
finalTsKvQueryList = new ArrayList<>(tsKvQueryList); finalTsKvQueryList = new ArrayList<>(tsKvQueryList);
finalTsKvQueryList.addAll(cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( finalTsKvQueryList.addAll(keys.stream().map(key -> {
key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg() var query = new BaseReadTsKvQuery(
)).collect(Collectors.toList())); 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 { } else {
finalTsKvQueryList = tsKvQueryList; finalTsKvQueryList = tsKvQueryList;
} }
Map<EntityData, ListenableFuture<List<TsKvEntry>>> fetchResultMap = new HashMap<>(); Map<EntityData, ListenableFuture<List<ReadTsKvQueryResult>>> fetchResultMap = new HashMap<>();
ctx.getData().getData().forEach(entityData -> fetchResultMap.put(entityData, List<EntityData> entityDataList = ctx.getData().getData();
tsService.findAll(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList))); entityDataList.forEach(entityData -> fetchResultMap.put(entityData,
tsService.findAllByQueries(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList)));
return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> { 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) -> { fetchResultMap.forEach((entityData, future) -> {
Map<String, List<TsValue>> keyData = new LinkedHashMap<>();
cmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>()));
try { try {
List<TsKvEntry> entityTsData = future.get(); Map<String, Long> lastTsMap = new HashMap<>();
if (entityTsData != null) { lastTsEntityMap.put(entityData, lastTsMap);
entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString())));
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()) { if (cmd.isFetchLatestPreviousPoint()) {
entityData.getTimeseries().values().forEach(dataArray -> { entityData.getTimeseries().values().forEach(dataArray -> Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs())));
Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs()));
});
} }
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), 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()); update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.setInitialDataSent(true); ctx.setInitialDataSent(true);
} else { } else {
update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); update = new EntityDataUpdate(ctx.getCmdId(), null, entityDataList, ctx.getMaxEntitiesPerDataSubscription());
} }
if (subscribe) { 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.sendWsMsg(update);
ctx.getData().getData().forEach(ed -> ed.getTimeseries().clear()); entityDataList.forEach(ed -> ed.getTimeseries().clear());
} finally { } finally {
ctx.getWsLock().unlock(); ctx.getWsLock().unlock();
} }
@ -533,11 +662,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
ctx.getWsLock().lock(); ctx.getWsLock().lock();
try { try {
ctx.createLatestValuesSubscriptions(latestCmd.getKeys()); ctx.createLatestValuesSubscriptions(latestCmd.getKeys());
if (!ctx.isInitialDataSent()) { checkAndSendInitialData(ctx);
EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.sendWsMsg(update);
ctx.setInitialDataSent(true);
}
} finally { } finally {
ctx.getWsLock().unlock(); 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 { } else {
oldDataMap = Collections.emptyMap(); 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())) { if (oldDataMap.size() == newDataMap.size() && oldDataMap.keySet().equals(newDataMap.keySet())) {
log.trace("[{}][{}] No updates to entity data found", sessionRef.getSessionId(), cmdId); log.trace("[{}][{}] No updates to entity data found", sessionRef.getSessionId(), cmdId);
} else { } else {
@ -122,8 +122,17 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
createSubscriptions(keys, true, 0, 0); createSubscriptions(keys, true, 0, 0);
} }
public void createTimeseriesSubscriptions(List<EntityKey> keys, long startTs, long endTs) { public void createTimeSeriesSubscriptions(Map<EntityData, Map<String, Long>> entityKeyStates, long startTs, long endTs) {
createSubscriptions(keys, false, startTs, 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) { 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); 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); log.trace("[{}][{}][{}] Creating time-series subscription for [{}] with keys: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), keyStates);
return TbTimeseriesSubscription.builder() return TbTimeseriesSubscription.builder()
.serviceId(serviceId) .serviceId(serviceId)
@ -198,7 +215,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
.subscriptionId(subIdx) .subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId()) .tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityData.getEntityId()) .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) .allKeys(false)
.keyStates(keyStates) .keyStates(keyStates)
.latestValues(latestValues) .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!"); "Cmd id is negative value!");
sendWsMsg(sessionRef, update); sendWsMsg(sessionRef, update);
return false; 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, TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Query is empty!"); "Query is empty!");
sendWsMsg(sessionRef, update); 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; package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter; import lombok.Getter;
import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -30,17 +31,35 @@ public class EntityDataCmd extends DataCmd {
private final LatestValueCmd latestCmd; private final LatestValueCmd latestCmd;
@Getter @Getter
private final TimeSeriesCmd tsCmd; 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 @JsonCreator
public EntityDataCmd(@JsonProperty("cmdId") int cmdId, public EntityDataCmd(@JsonProperty("cmdId") int cmdId,
@JsonProperty("query") EntityDataQuery query, @JsonProperty("query") EntityDataQuery query,
@JsonProperty("historyCmd") EntityHistoryCmd historyCmd, @JsonProperty("historyCmd") EntityHistoryCmd historyCmd,
@JsonProperty("latestCmd") LatestValueCmd latestCmd, @JsonProperty("latestCmd") LatestValueCmd latestCmd,
@JsonProperty("tsCmd") TimeSeriesCmd tsCmd) { @JsonProperty("tsCmd") TimeSeriesCmd tsCmd,
@JsonProperty("aggHistoryCmd") AggHistoryCmd aggHistoryCmd,
@JsonProperty("aggTsCmd") AggTimeSeriesCmd aggTsCmd) {
super(cmdId); super(cmdId);
this.query = query; this.query = query;
this.historyCmd = historyCmd; this.historyCmd = historyCmd;
this.latestCmd = latestCmd; this.latestCmd = latestCmd;
this.tsCmd = tsCmd; 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.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -32,6 +33,8 @@ import java.util.List;
*/ */
public interface TimeseriesService { 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>> findAll(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys); 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"); this(key, startTs, endTs, interval, limit, aggregation, "DESC");
} }
public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, String order) {
String order) {
super(key, startTs, endTs); super(key, startTs, endTs);
this.interval = interval; this.interval = interval;
this.limit = limit; 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 @Data
public class BaseTsKvQuery implements TsKvQuery { public class BaseTsKvQuery implements TsKvQuery {
private static final ThreadLocal<Integer> idSeq = ThreadLocal.withInitial(() -> 0);
private final int id;
private final String key; private final String key;
private final long startTs; private final long startTs;
private final long endTs; private final long endTs;
public BaseTsKvQuery(String key, long startTs, long endTs) { public BaseTsKvQuery(String key, long startTs, long endTs) {
this.id = idSeq.get();
idSeq.set(id + 1);
this.key = key; this.key = key;
this.startTs = startTs; this.startTs = startTs;
this.endTs = endTs; 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 { public class BasicTsKvEntry implements TsKvEntry {
private static final int MAX_CHARS_PER_DATA_POINT = 512; private static final int MAX_CHARS_PER_DATA_POINT = 512;
private final long ts; protected final long ts;
private final KvEntry kv; private final KvEntry kv;
public BasicTsKvEntry(long ts, 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; package org.thingsboard.server.common.data.kv;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import org.thingsboard.server.common.data.query.TsValue;
/** /**
* Represents time series KV data entry * Represents time series KV data entry
* *
* @author ashvayka * @author ashvayka
* *
*/ */
@ -30,4 +31,9 @@ public interface TsKvEntry extends KvEntry {
@JsonIgnore @JsonIgnore
int getDataPoints(); 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 { public interface TsKvQuery {
int getId();
String getKey(); String getKey();
long getStartTs(); 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; package org.thingsboard.server.common.data.query;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import java.util.Map; import java.util.Map;
@Data @Data
@RequiredArgsConstructor
public class EntityData { public class EntityData {
private final EntityId entityId; private final EntityId entityId;
private final Map<EntityKeyType, Map<String, TsValue>> latest; private final Map<EntityKeyType, Map<String, TsValue>> latest;
private final Map<String, TsValue[]> timeseries; 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 @ApiModel
@Data @Data
public class EntityKey implements Serializable { public class EntityKey implements Serializable {
private static final long serialVersionUID = -6421575477523085543L;
private final EntityKeyType type; private final EntityKeyType type;
private final String key; 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; package org.thingsboard.server.common.data.query;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor;
@Data @Data
@RequiredArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TsValue { public class TsValue {
public static final TsValue EMPTY = new TsValue(0, "");
private final long ts; private final long ts;
private final String value; 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[] 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 = 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)}); 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; package org.thingsboard.server.dao.model.sql;
import lombok.Data; 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.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
@ -80,6 +81,18 @@ public abstract class AbstractTsKvEntity implements ToData<TsKvEntry> {
@Transient @Transient
protected String strKey; protected String strKey;
@Transient
protected Long aggValuesLastTs;
@Transient
protected Long aggValuesCount;
public AbstractTsKvEntity() {
}
public AbstractTsKvEntity(Long aggValuesLastTs) {
this.aggValuesLastTs = aggValuesLastTs;
}
public abstract boolean isNotEmpty(); public abstract boolean isNotEmpty();
protected static boolean isAllNull(Object... args) { protected static boolean isAllNull(Object... args) {
@ -105,7 +118,12 @@ public abstract class AbstractTsKvEntity implements ToData<TsKvEntry> {
} else if (jsonValue != null) { } else if (jsonValue != null) {
kvEntry = new JsonDataEntry(strKey, jsonValue); 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 = "doubleCountValue", type = Long.class),
@ColumnResult(name = "strValue", type = String.class), @ColumnResult(name = "strValue", type = String.class),
@ColumnResult(name = "aggType", 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 = "longValueCount", type = Long.class),
@ColumnResult(name = "doubleValueCount", type = Long.class), @ColumnResult(name = "doubleValueCount", type = Long.class),
@ColumnResult(name = "jsonValueCount", 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() {
} }
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)) { if (!StringUtils.isEmpty(strValue)) {
this.strValue = strValue; this.strValue = strValue;
} }
@ -135,6 +138,7 @@ public final class TimescaleTsKvEntity extends AbstractTsKvEntity {
} else { } else {
this.doubleValue = 0.0; this.doubleValue = 0.0;
} }
this.aggValuesCount = totalCount;
break; break;
case SUM: case SUM:
if (doubleCountValue > 0) { 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)) { if (!isAllNull(tsBucket, interval, booleanValueCount, strValueCount, longValueCount, doubleValueCount, jsonValueCount)) {
this.ts = tsBucket + interval / 2; this.ts = tsBucket + interval / 2;
if (booleanValueCount != 0) { 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; package org.thingsboard.server.dao.model.sqlts.ts;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.IdClass; import javax.persistence.IdClass;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Transient;
@EqualsAndHashCode(callSuper = true)
@Data @Data
@Entity @Entity
@Table(name = "ts_kv") @Table(name = "ts_kv")
@ -31,11 +34,13 @@ public final class TsKvEntity extends AbstractTsKvEntity {
public TsKvEntity() { public TsKvEntity() {
} }
public TsKvEntity(String strValue) { public TsKvEntity(String strValue, Long aggValuesLastTs) {
super(aggValuesLastTs);
this.strValue = strValue; 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)) { if (!isAllNull(longValue, doubleValue, longCountValue, doubleCountValue)) {
switch (aggType) { switch (aggType) {
case AVG: case AVG:
@ -52,6 +57,7 @@ public final class TsKvEntity extends AbstractTsKvEntity {
} else { } else {
this.doubleValue = 0.0; this.doubleValue = 0.0;
} }
this.aggValuesCount = totalCount;
break; break;
case SUM: case SUM:
if (doubleCountValue > 0) { 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 (!isAllNull(booleanValueCount, strValueCount, longValueCount, doubleValueCount)) {
if (booleanValueCount != 0) { if (booleanValueCount != 0) {
this.longValue = booleanValueCount; 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")); EntityType entityType = EntityType.valueOf((String) row.get("entity_type"));
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, id); EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, id);
Map<EntityKeyType, Map<String, TsValue>> latest = new HashMap<>(); Map<EntityKeyType, Map<String, TsValue>> latest = new HashMap<>();
Map<String, TsValue[]> timeseries = new HashMap<>(); //Maybe avoid empty hashmaps?
EntityData entityData = new EntityData(entityId, latest, timeseries); EntityData entityData = new EntityData(entityId, latest, new HashMap<>(), new HashMap<>());
for (EntityKeyMapping mapping : selectionMapping) { for (EntityKeyMapping mapping : selectionMapping) {
if (!mapping.isIgnore()) { if (!mapping.isIgnore()) {
EntityKey entityKey = mapping.getEntityKey(); 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; 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 lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; 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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.Aggregation; 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.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
@ -47,10 +45,9 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j @Slf4j
public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSqlTimeseriesDao implements TimeseriesDao { 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) Comparator.comparing((Function<TsKvEntity, UUID>) AbstractTsKvEntity::getEntityId)
.thenComparing(AbstractTsKvEntity::getKey) .thenComparing(AbstractTsKvEntity::getKey)
.thenComparing(AbstractTsKvEntity::getTs) .thenComparing(AbstractTsKvEntity::getTs)
); );
} }
@PreDestroy @PreDestroy
@ -114,32 +111,32 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
} }
@Override @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); return processFindAllAsync(tenantId, entityId, queries);
} }
@Override @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) { if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(entityId, query); return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
} else { } else {
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); List<ListenableFuture<Optional<TsKvEntity>>> futures = new ArrayList<>();
long endPeriod = query.getEndTs();
long startPeriod = query.getStartTs(); long startPeriod = query.getStartTs();
long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs());
long step = query.getInterval(); long step = query.getInterval();
while (startPeriod <= endPeriod) { while (startPeriod < endPeriod) {
long startTs = startPeriod; long startTs = startPeriod;
long endTs = Math.min(startPeriod + step, endPeriod + 1); long endTs = Math.min(startPeriod + step, endPeriod);
long ts = startTs + (endTs - startTs) / 2; 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); futures.add(aggregateTsKvEntry);
startPeriod = endTs; 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()); Integer keyId = getOrSaveKeyId(query.getKey());
List<TsKvEntity> tsKvEntities = tsKvRepository.findAllWithLimit( List<TsKvEntity> tsKvEntities = tsKvRepository.findAllWithLimit(
entityId.getId(), entityId.getId(),
@ -147,125 +144,52 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
query.getStartTs(), query.getStartTs(),
query.getEndTs(), query.getEndTs(),
PageRequest.of(0, query.getLimit(), 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())); 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) { ListenableFuture<Optional<TsKvEntity>> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) {
List<CompletableFuture<TsKvEntity>> entitiesFutures = new ArrayList<>(); return service.submit(() -> {
switchAggregation(entityId, key, startTs, endTs, aggregation, entitiesFutures); TsKvEntity entity = switchAggregation(entityId, key, startTs, endTs, aggregation);
return Futures.transform(setFutures(entitiesFutures), entity -> {
if (entity != null && entity.isNotEmpty()) { if (entity != null && entity.isNotEmpty()) {
entity.setEntityId(entityId.getId()); entity.setEntityId(entityId.getId());
entity.setStrKey(key); entity.setStrKey(key);
entity.setTs(ts); entity.setTs(ts);
return Optional.of(DaoUtil.getData(entity)); return Optional.of(entity);
} else { } else {
return Optional.empty(); 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) { switch (aggregation) {
case AVG: case AVG:
findAvg(entityId, key, startTs, endTs, entitiesFutures); return tsKvRepository.findAvg(entityId.getId(), keyId, startTs, endTs);
break;
case MAX: case MAX:
findMax(entityId, key, startTs, endTs, entitiesFutures); var max = tsKvRepository.findNumericMax(entityId.getId(), keyId, startTs, endTs);
break; if (max.isNotEmpty()) {
return max;
} else {
return tsKvRepository.findStringMax(entityId.getId(), keyId, startTs, endTs);
}
case MIN: case MIN:
findMin(entityId, key, startTs, endTs, entitiesFutures); var min = tsKvRepository.findNumericMin(entityId.getId(), keyId, startTs, endTs);
break; if (min.isNotEmpty()) {
return min;
} else {
return tsKvRepository.findStringMin(entityId.getId(), keyId, startTs, endTs);
}
case SUM: case SUM:
findSum(entityId, key, startTs, endTs, entitiesFutures); return tsKvRepository.findSum(entityId.getId(), keyId, startTs, endTs);
break;
case COUNT: case COUNT:
findCount(entityId, key, startTs, endTs, entitiesFutures); return tsKvRepository.findCount(entityId.getId(), keyId, startTs, endTs);
break;
default: default:
throw new IllegalArgumentException("Not supported aggregation type: " + aggregation); 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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent;
@ -38,6 +39,7 @@ import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j @Slf4j
public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseriesDao implements AggregationTimeseriesDao { 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) { protected ListenableFuture<List<ReadTsKvQueryResult>> processFindAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries List<ListenableFuture<ReadTsKvQueryResult>> futures = queries
.stream() .stream()
.map(query -> findAllAsync(tenantId, entityId, query)) .map(query -> findAllAsync(tenantId, entityId, query))
.collect(Collectors.toList()); .collect(Collectors.toList());
return Futures.transform(Futures.allAsList(futures), new Function<>() { return Futures.transform(Futures.allAsList(futures), new Function<>() {
@Nullable @Nullable
@Override @Override
public List<TsKvEntry> apply(@Nullable List<List<TsKvEntry>> results) { public List<ReadTsKvQueryResult> apply(@Nullable List<ReadTsKvQueryResult> results) {
if (results == null || results.isEmpty()) { if (results == null || results.isEmpty()) {
return null; return null;
} }
return results.stream() return results.stream().filter(Objects::nonNull).collect(Collectors.toList());
.filter(Objects::nonNull)
.flatMap(List::stream)
.collect(Collectors.toList());
} }
}, service); }, 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.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import java.util.List; import java.util.List;
public interface AggregationTimeseriesDao { 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.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException; 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.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.TsKvDictionary;
import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionaryCompositeKey; 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.sql.JpaAbstractDaoListeningExecutorService;
import org.thingsboard.server.dao.sqlts.dictionary.TsKvDictionaryRepository; import org.thingsboard.server.dao.sqlts.dictionary.TsKvDictionaryRepository;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@ -80,18 +86,20 @@ public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeni
return keyId; return keyId;
} }
protected ListenableFuture<List<TsKvEntry>> getTskvEntriesFuture(ListenableFuture<List<Optional<TsKvEntry>>> future) { protected ListenableFuture<ReadTsKvQueryResult> getReadTsKvQueryResultFuture(ReadTsKvQuery query, ListenableFuture<List<Optional<? extends AbstractTsKvEntity>>> future) {
return Futures.transform(future, new Function<List<Optional<TsKvEntry>>, List<TsKvEntry>>() { return Futures.transform(future, new Function<>() {
@Nullable @Nullable
@Override @Override
public List<TsKvEntry> apply(@Nullable List<Optional<TsKvEntry>> results) { public ReadTsKvQueryResult apply(@Nullable List<Optional<? extends AbstractTsKvEntity>> results) {
if (results == null || results.isEmpty()) { if (results == null || results.isEmpty()) {
return null; return null;
} }
return results.stream() List<? extends AbstractTsKvEntity> data = results.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
.filter(Optional::isPresent) var lastTs = data.stream().map(AbstractTsKvEntity::getAggValuesLastTs).filter(Objects::nonNull).max(Long::compare);
.map(Optional::get) if (lastTs.isEmpty()) {
.collect(Collectors.toList()); 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); }, 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.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
@ -190,7 +191,8 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
long endTs = query.getStartTs() - 1; long endTs = query.getStartTs() - 1;
ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1,
Aggregation.NONE, DESC_ORDER); 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) { 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_SUM = "findSum";
public static final String FIND_COUNT = "findCount"; 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 FROM_WHERE_CLAUSE = "FROM ts_kv tskv WHERE " +
"tskv.entity_id = cast(:entityId AS uuid) " +
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 "; "AND tskv.key= cast(:entityKey AS int) " +
"AND tskv.ts >= :startTs AND tskv.ts < :endTs " +
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 "; "GROUP BY tskv.entity_id, tskv.key, tsBucket " +
"ORDER BY tskv.entity_id, tskv.key, tsBucket";
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_AVG_QUERY = "SELECT " +
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 "; "time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " +
"SUM(COALESCE(tskv.long_v, 0)) AS longValue, " +
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 "; "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 @PersistenceContext
private EntityManager entityManager; private EntityManager entityManager;
@Async @SuppressWarnings("unchecked")
public CompletableFuture<List<TimescaleTsKvEntity>> findAvg(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { public List<TimescaleTsKvEntity> findAvg(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked") return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_AVG);
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_AVG);
return CompletableFuture.supplyAsync(() -> resultList);
} }
@Async @SuppressWarnings("unchecked")
public CompletableFuture<List<TimescaleTsKvEntity>> findMax(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { public List<TimescaleTsKvEntity> findMax(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked") return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MAX);
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MAX);
return CompletableFuture.supplyAsync(() -> resultList);
} }
@Async @SuppressWarnings("unchecked")
public CompletableFuture<List<TimescaleTsKvEntity>> findMin(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { public List<TimescaleTsKvEntity> findMin(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked") return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MIN);
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MIN);
return CompletableFuture.supplyAsync(() -> resultList);
} }
@Async @SuppressWarnings("unchecked")
public CompletableFuture<List<TimescaleTsKvEntity>> findSum(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { public List<TimescaleTsKvEntity> findSum(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked") return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_SUM);
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_SUM);
return CompletableFuture.supplyAsync(() -> resultList);
} }
@Async @SuppressWarnings("unchecked")
public CompletableFuture<List<TimescaleTsKvEntity>> findCount(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { public List<TimescaleTsKvEntity> findCount(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) {
@SuppressWarnings("unchecked") return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_COUNT);
List<TimescaleTsKvEntity> resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_COUNT);
return CompletableFuture.supplyAsync(() -> resultList);
} }
private List getResultList(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs, String query) { private List getResultList(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs, String query) {
@ -96,5 +121,4 @@ public class AggregationRepository {
.getResultList(); .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.Aggregation;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; 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.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.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao; import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
@ -101,13 +103,13 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
} }
@Override @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); return processFindAllAsync(tenantId, entityId, queries);
} }
@Override @Override
public ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { 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(); String strKey = tsKvEntry.getKey();
Integer keyId = getOrSaveKeyId(strKey); Integer keyId = getOrSaveKeyId(strKey);
TimescaleTsKvEntity entity = new TimescaleTsKvEntity(); TimescaleTsKvEntity entity = new TimescaleTsKvEntity();
@ -148,15 +150,15 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
} }
@Override @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) { if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(entityId, query); return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query));
} else { } else {
long startTs = query.getStartTs(); long startTs = query.getStartTs();
long endTs = query.getEndTs(); long endTs = Math.max(query.getStartTs() + 1, query.getEndTs());
long timeBucket = query.getInterval(); long timeBucket = query.getInterval();
ListenableFuture<List<Optional<TsKvEntry>>> future = findAllAndAggregateAsync(entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation()); List<Optional<? extends AbstractTsKvEntity>> data = findAllAndAggregateAsync(entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation());
return getTskvEntriesFuture(future); return getReadTsKvQueryResultFuture(query, Futures.immediateFuture(data));
} }
} }
@ -165,7 +167,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
super.cleanup(systemTtl); super.cleanup(systemTtl);
} }
private ListenableFuture<List<TsKvEntry>> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { private ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) {
String strKey = query.getKey(); String strKey = query.getKey();
Integer keyId = getOrSaveKeyId(strKey); Integer keyId = getOrSaveKeyId(strKey);
List<TimescaleTsKvEntity> timescaleTsKvEntities = tsKvRepository.findAllWithLimit( List<TimescaleTsKvEntity> timescaleTsKvEntities = tsKvRepository.findAllWithLimit(
@ -174,105 +176,60 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
query.getStartTs(), query.getStartTs(),
query.getEndTs(), query.getEndTs(),
PageRequest.of(0, query.getLimit(), 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)); 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) { private List<Optional<? extends AbstractTsKvEntity>> 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()); long interval = endTs - startTs;
SettableFuture<List<TimescaleTsKvEntity>> listenableFuture = SettableFuture.create(); long remainingPart = interval % timeBucket;
listCompletableFuture.whenComplete((timescaleTsKvEntities, throwable) -> { List<TimescaleTsKvEntity> timescaleTsKvEntities;
if (throwable != null) { if (remainingPart == 0) {
listenableFuture.setException(throwable); timescaleTsKvEntities = switchAggregation(key, startTs, endTs, timeBucket, aggregation, entityId.getId());
} else { } else {
listenableFuture.set(timescaleTsKvEntities); interval = interval - remainingPart;
} timescaleTsKvEntities = new ArrayList<>();
}); timescaleTsKvEntities.addAll(switchAggregation(key, startTs, startTs + interval, timeBucket, aggregation, entityId.getId()));
return Futures.transform(listenableFuture, timescaleTsKvEntities -> { timescaleTsKvEntities.addAll(switchAggregation(key, startTs + interval, endTs, remainingPart, aggregation, entityId.getId()));
if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) { }
List<Optional<TsKvEntry>> result = new ArrayList<>();
timescaleTsKvEntities.forEach(entity -> { if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) {
if (entity != null && entity.isNotEmpty()) { List<Optional<? extends AbstractTsKvEntity>> result = new ArrayList<>();
entity.setEntityId(entityId.getId()); timescaleTsKvEntities.forEach(entity -> {
entity.setStrKey(key); if (entity != null && entity.isNotEmpty()) {
result.add(Optional.of(DaoUtil.getData(entity))); entity.setEntityId(entityId.getId());
} else { entity.setStrKey(key);
result.add(Optional.empty()); result.add(Optional.of(entity));
} } else {
}); result.add(Optional.empty());
return result; }
} else { });
return Collections.emptyList(); return result;
} } else {
}, MoreExecutors.directExecutor()); 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) { switch (aggregation) {
case AVG: case AVG:
return findAvg(key, startTs, endTs, timeBucket, entityId); return aggregationRepository.findAvg(entityId, keyId, timeBucket, startTs, endTs);
case MAX: case MAX:
return findMax(key, startTs, endTs, timeBucket, entityId); return aggregationRepository.findMax(entityId, keyId, timeBucket, startTs, endTs);
case MIN: case MIN:
return findMin(key, startTs, endTs, timeBucket, entityId); return aggregationRepository.findMin(entityId, keyId, timeBucket, startTs, endTs);
case SUM: case SUM:
return findSum(key, startTs, endTs, timeBucket, entityId); return aggregationRepository.findSum(entityId, keyId, timeBucket, startTs, endTs);
case COUNT: case COUNT:
return findCount(key, startTs, endTs, timeBucket, entityId); return aggregationRepository.findCount(entityId, keyId, timeBucket, startTs, endTs);
default: default:
throw new IllegalArgumentException("Not supported aggregation type: " + aggregation); 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.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvCompositeKey; import org.thingsboard.server.dao.model.sqlts.ts.TsKvCompositeKey;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public interface TsKvRepository extends JpaRepository<TsKvEntity, TsKvCompositeKey> { public interface TsKvRepository extends JpaRepository<TsKvEntity, TsKvCompositeKey> {
@ -48,82 +46,75 @@ public interface TsKvRepository extends JpaRepository<TsKvEntity, TsKvCompositeK
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @Param("endTs") long endTs);
@Async @Query("SELECT new TsKvEntity(MAX(tskv.strValue), MAX(tskv.ts)) FROM TsKvEntity tskv " +
@Query("SELECT new TsKvEntity(MAX(tskv.strValue)) FROM TsKvEntity tskv " +
"WHERE tskv.strValue IS NOT NULL " + "WHERE tskv.strValue IS NOT NULL " +
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") "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("entityKey") int entityKey,
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(MAX(COALESCE(tskv.longValue, -9223372036854775807)), " + @Query("SELECT new TsKvEntity(MAX(COALESCE(tskv.longValue, -9223372036854775807)), " +
"MAX(COALESCE(tskv.doubleValue, -1.79769E+308)), " + "MAX(COALESCE(tskv.doubleValue, -1.79769E+308)), " +
"SUM(CASE WHEN tskv.longValue 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.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") "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("entityKey") int entityKey,
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @Param("endTs") long endTs);
@Async @Query("SELECT new TsKvEntity(MIN(tskv.strValue), MAX(tskv.ts)) FROM TsKvEntity tskv " +
@Query("SELECT new TsKvEntity(MIN(tskv.strValue)) FROM TsKvEntity tskv " +
"WHERE tskv.strValue IS NOT NULL " + "WHERE tskv.strValue IS NOT NULL " +
"AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") "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("entityKey") int entityKey,
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(MIN(COALESCE(tskv.longValue, 9223372036854775807)), " + @Query("SELECT new TsKvEntity(MIN(COALESCE(tskv.longValue, 9223372036854775807)), " +
"MIN(COALESCE(tskv.doubleValue, 1.79769E+308)), " + "MIN(COALESCE(tskv.doubleValue, 1.79769E+308)), " +
"SUM(CASE WHEN tskv.longValue 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.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") "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("entityId") UUID entityId,
@Param("entityKey") int entityKey, @Param("entityKey") int entityKey,
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(SUM(CASE WHEN tskv.booleanValue IS NULL THEN 0 ELSE 1 END), " + @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.strValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.longValue 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.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") "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("entityKey") int entityKey,
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " + @Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " +
"SUM(COALESCE(tskv.doubleValue, 0.0)), " + "SUM(COALESCE(tskv.doubleValue, 0.0)), " +
"SUM(CASE WHEN tskv.longValue 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.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") "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("entityKey") int entityKey,
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @Param("endTs") long endTs);
@Async
@Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " + @Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " +
"SUM(COALESCE(tskv.doubleValue, 0.0)), " + "SUM(COALESCE(tskv.doubleValue, 0.0)), " +
"SUM(CASE WHEN tskv.longValue 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.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") "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("entityKey") int entityKey,
@Param("startTs") long startTs, @Param("startTs") long startTs,
@Param("endTs") long endTs); @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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.kv.AggTsKvEntry;
import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; 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.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.nosql.TbResultSet;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -40,18 +42,20 @@ import java.util.stream.Collectors;
* Created by ashvayka on 20.02.17. * Created by ashvayka on 20.02.17.
*/ */
@Slf4j @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 LONG_CNT_POS = 0;
private static final int DOUBLE_CNT_POS = 1; private static final int DOUBLE_CNT_POS = 1;
private static final int BOOL_CNT_POS = 2; private static final int BOOL_CNT_POS = 2;
private static final int STR_CNT_POS = 3; private static final int STR_CNT_POS = 3;
private static final int JSON_CNT_POS = 4; private static final int JSON_CNT_POS = 4;
private static final int LONG_POS = 5; private static final int MAX_TS_POS = 5;
private static final int DOUBLE_POS = 6; private static final int LONG_POS = 6;
private static final int BOOL_POS = 7; private static final int DOUBLE_POS = 7;
private static final int STR_POS = 8; private static final int BOOL_POS = 8;
private static final int JSON_POS = 9; private static final int STR_POS = 9;
private static final int JSON_POS = 10;
private final Aggregation aggregation; private final Aggregation aggregation;
private final String key; private final String key;
@ -66,29 +70,29 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
} }
@Override @Override
public ListenableFuture<Optional<TsKvEntry>> apply(@Nullable List<TbResultSet> rsList) { public ListenableFuture<Optional<TsKvEntryAggWrapper>> apply(@Nullable List<TbResultSet> rsList) {
log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation); log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation);
if (rsList == null || rsList.isEmpty()) { if (rsList == null || rsList.isEmpty()) {
return Futures.immediateFuture(Optional.empty()); return Futures.immediateFuture(Optional.empty());
} }
return Futures.transform( return Futures.transform(
Futures.allAsList( Futures.allAsList(
rsList.stream().map(rs -> rs.allRows(this.executor)) rsList.stream().map(rs -> rs.allRows(this.executor))
.collect(Collectors.toList())), .collect(Collectors.toList())),
rowsList -> { rowsList -> {
try { try {
AggregationResult aggResult = new AggregationResult(); AggregationResult aggResult = new AggregationResult();
for (List<Row> rs : rowsList) { for (List<Row> rs : rowsList) {
for (Row row : rs) { for (Row row : rs) {
processResultSetRow(row, aggResult); processResultSetRow(row, aggResult);
}
}
return processAggregationResult(aggResult);
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e);
return Optional.empty();
} }
} }, this.executor);
return processAggregationResult(aggResult);
} catch (Exception e) {
log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e);
return Optional.empty();
}
}, this.executor);
} }
private void processResultSetRow(Row row, AggregationResult aggResult) { 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 boolCount = row.getLong(BOOL_CNT_POS);
long strCount = row.getLong(STR_CNT_POS); long strCount = row.getLong(STR_CNT_POS);
long jsonCount = row.getLong(JSON_CNT_POS); long jsonCount = row.getLong(JSON_CNT_POS);
long aggValuesLastTs = row.getLong(MAX_TS_POS);
if (longCount > 0 || doubleCount > 0) { if (longCount > 0 || doubleCount > 0) {
if (longCount > 0) { if (longCount > 0) {
@ -134,6 +139,8 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
return; return;
} }
aggResult.aggValuesLastTs = Math.max(aggResult.aggValuesLastTs, aggValuesLastTs);
if (aggregation == Aggregation.COUNT) { if (aggregation == Aggregation.COUNT) {
aggResult.count += curCount; aggResult.count += curCount;
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) { } 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; Optional<TsKvEntry> result;
if (aggResult.dataType == null) { if (aggResult.dataType == null) {
result = Optional.empty(); result = Optional.empty();
} else if (aggregation == Aggregation.COUNT) { } else if (aggregation == Aggregation.COUNT) {
result = Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggResult.count))); result = Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggResult.count)));
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) { } else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) {
result = processAvgOrSumResult(aggResult); result = processAvgOrSumResult(aggregation, aggResult);
} else if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) { } else if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) {
result = processMinOrMaxResult(aggResult); result = processMinOrMaxResult(aggResult);
} else { } else {
result = Optional.empty(); result = Optional.empty();
} }
if (!result.isPresent()) { if (result.isEmpty()) {
log.trace("[{}][{}][{}] Aggregated data is empty.", key, ts, aggregation); 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)) { if (aggResult.count == 0 || (aggResult.dataType == DataType.DOUBLE && aggResult.dValue == null) || (aggResult.dataType == DataType.LONG && aggResult.lValue == null)) {
return Optional.empty(); return Optional.empty();
} else if (aggResult.dataType == DataType.DOUBLE || aggResult.dataType == DataType.LONG) { } else if (aggResult.dataType == DataType.DOUBLE || aggResult.dataType == DataType.LONG) {
if (aggregation == Aggregation.AVG || aggResult.hasDouble) { if (aggregation == Aggregation.AVG || aggResult.hasDouble) {
double sum = Optional.ofNullable(aggResult.dValue).orElse(0.0d) + Optional.ofNullable(aggResult.lValue).orElse(0L); 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 { } 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(); return Optional.empty();
@ -291,5 +301,6 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
Long lValue = null; Long lValue = null;
long count = 0; long count = 0;
boolean hasDouble = false; 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.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.dao.entityview.EntityViewService; 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.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isBlank; 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 * @author Andrew Shvayka
*/ */
@SuppressWarnings("UnstableApiUsage")
@Service @Service
@Slf4j @Slf4j
public class BaseTimeseriesService implements TimeseriesService { 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 = 3;
private static final int INSERTS_PER_ENTRY_WITHOUT_LATEST = 2; private static final int INSERTS_PER_ENTRY_WITHOUT_LATEST = 2;
private static final int DELETES_PER_ENTRY = INSERTS_PER_ENTRY; 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 @Override
public @Nullable Integer apply(@Nullable List<Integer> input) { public @Nullable Integer apply(@Nullable List<Integer> input) {
int result = 0; int result = 0;
@ -87,7 +90,7 @@ public class BaseTimeseriesService implements TimeseriesService {
private EntityViewService entityViewService; private EntityViewService entityViewService;
@Override @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); validate(entityId);
queries.forEach(this::validate); queries.forEach(this::validate);
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) {
@ -103,6 +106,17 @@ public class BaseTimeseriesService implements TimeseriesService {
return timeseriesDao.findAllAsync(tenantId, entityId, queries); 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 @Override
public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) { public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId); validate(entityId);
@ -244,7 +258,7 @@ public class BaseTimeseriesService implements TimeseriesService {
public ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId) { public ListenableFuture<Collection<String>> removeAllLatest(TenantId tenantId, EntityId entityId) {
validate(entityId); validate(entityId);
return Futures.transformAsync(this.findAllLatest(tenantId, entityId), latest -> { 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()); Collection<String> keys = latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList());
return Futures.transform(this.removeLatest(tenantId, entityId, keys), res -> keys, MoreExecutors.directExecutor()); return Futures.transform(this.removeLatest(tenantId, entityId, keys), res -> keys, MoreExecutors.directExecutor());
} else { } 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.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.nosql.TbResultSet;
import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.nosql.TbResultSetFuture;
@ -71,6 +73,7 @@ import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
@SuppressWarnings("UnstableApiUsage")
@Component @Component
@Slf4j @Slf4j
@NoSqlTsDao @NoSqlTsDao
@ -139,20 +142,10 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
} }
@Override @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) {
List<ListenableFuture<List<TsKvEntry>>> futures = queries.stream().map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList()); List<ListenableFuture<ReadTsKvQueryResult>> futures = queries.stream()
return Futures.transform(Futures.allAsList(futures), new Function<>() { .map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList());
@Nullable return Futures.allAsList(futures);
@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);
} }
@Override @Override
@ -270,28 +263,42 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
} }
@Override @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) { if (query.getAggregation() == Aggregation.NONE) {
return findAllAsyncWithLimit(tenantId, entityId, query); return findAllAsyncWithLimit(tenantId, entityId, query);
} else { } else {
long startPeriod = query.getStartTs(); 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); long step = Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS);
List<ListenableFuture<Optional<TsKvEntry>>> futures = new ArrayList<>(); List<ListenableFuture<Optional<TsKvEntryAggWrapper>>> futures = new ArrayList<>();
while (startPeriod <= endPeriod) { while (startPeriod < endPeriod) {
long startTs = startPeriod; long startTs = startPeriod;
long endTs = Math.min(startPeriod + step, endPeriod + 1); long endTs = Math.min(startPeriod + step, endPeriod);
long ts = endTs - startTs; long ts = endTs - startTs;
ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, ts, 1, query.getAggregation(), query.getOrder()); ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, ts, 1, query.getAggregation(), query.getOrder());
futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs)));
startPeriod = 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<>() { return Futures.transform(future, new Function<>() {
@Nullable @Nullable
@Override @Override
public List<TsKvEntry> apply(@Nullable List<Optional<TsKvEntry>> input) { public ReadTsKvQueryResult apply(@Nullable List<Optional<TsKvEntryAggWrapper>> input) {
return input == null ? Collections.emptyList() : input.stream().filter(v -> v.isPresent()).map(v -> v.get()).collect(Collectors.toList()); 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); }, readResultsProcessingExecutor);
} }
@ -302,13 +309,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
//Cleanup by TTL is native for Cassandra //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 minPartition = toPartitionTs(query.getStartTs());
long maxPartition = toPartitionTs(query.getEndTs()); long maxPartition = toPartitionTs(query.getEndTs());
final ListenableFuture<List<Long>> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition); final ListenableFuture<List<Long>> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition);
final SimpleListenableFuture<List<TsKvEntry>> resultFuture = new SimpleListenableFuture<>(); final SimpleListenableFuture<List<TsKvEntry>> resultFuture = new SimpleListenableFuture<>();
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() { Futures.addCallback(partitionsListFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable List<Long> partitions) { public void onSuccess(@Nullable List<Long> partitions) {
TsKvQueryCursor cursor = new TsKvQueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions); TsKvQueryCursor cursor = new TsKvQueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions);
@ -321,7 +328,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
} }
}, readResultsProcessingExecutor); }, 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) { 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 Aggregation aggregation = query.getAggregation();
final String key = query.getKey(); final String key = query.getKey();
final long startTs = query.getStartTs(); 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.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
@ -145,9 +146,10 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
long endTs = query.getStartTs() - 1; long endTs = query.getStartTs() - 1;
ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1,
Aggregation.NONE, DESC_ORDER); 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) { if (entryList.size() == 1) {
TsKvEntry entry = entryList.get(0); TsKvEntry entry = entryList.get(0);
return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); 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.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
*/ */
public interface TimeseriesDao { 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); 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); 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(); List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(1, entries.size()); 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)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(1, entries.size()); 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 @Test
@ -240,14 +240,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get(); List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size()); Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); 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)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size()); Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); 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 @Test
@ -264,14 +264,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get(); List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size()); Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); 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)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size()); Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); 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 @Test
@ -286,14 +286,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get(); List<TsKvEntry> entries = tsService.findAll(tenantId, deviceId, queries).get();
Assert.assertEquals(2, entries.size()); Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); 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)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY));
entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); entries = tsService.findAll(tenantId, entityView.getId(), queries).get();
Assert.assertEquals(2, entries.size()); Assert.assertEquals(2, entries.size());
Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); 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 @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 com.google.common.util.concurrent.ListenableFuture;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; 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.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; 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.TsKvEntry;
import java.util.Optional; import java.util.Optional;
@ -43,25 +46,25 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
final int LIMIT = 1; final int LIMIT = 1;
final String TEMP = "temp"; final String TEMP = "temp";
final String DESC = "DESC"; final String DESC = "DESC";
AbstractChunkedAggregationTimeseriesDao tsDao; private AbstractChunkedAggregationTimeseriesDao tsDao;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
tsDao = spy(AbstractChunkedAggregationTimeseriesDao.class); tsDao = spy(AbstractChunkedAggregationTimeseriesDao.class);
ListenableFuture<Optional<TsKvEntry>> optionalListenableFuture = Futures.immediateFuture(Optional.of(mock(TsKvEntry.class))); Optional<TsKvEntry> optionalListenableFuture = Optional.of(mock(TsKvEntry.class));
willReturn(optionalListenableFuture).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any()); willReturn(Futures.immediateFuture(optionalListenableFuture)).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any());
willReturn(Futures.immediateFuture(mock(TsKvEntry.class))).given(tsDao).getTskvEntriesFuture(any()); willReturn(Futures.immediateFuture(mock(ReadTsKvQueryResult.class))).given(tsDao).getReadTsKvQueryResultFuture(any(), any());
} }
@Test @Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenLastIntervalShorterThanOthersAndEqualsEndTs() { public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenLastIntervalShorterThanOthersAndEqualsEndTs() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2000, LIMIT, COUNT, DESC); ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2000, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 2001, 1001, 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); tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(2)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); 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, 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 @Test
@ -71,19 +74,17 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
assertThat(tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query)).isNotNull(); 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(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 @Test
public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsPeriodMinusOne() { public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsPeriodMinusOne() {
ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2999, LIMIT, COUNT, DESC); ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2999, LIMIT, COUNT, DESC);
ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 3000, 1500, 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); willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
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, 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); willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
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(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 @Test
@ -134,7 +135,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
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(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 @Test
@ -144,8 +145,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest {
tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query);
verify(tsDao, times(1000)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); verify(tsDao, times(1000)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any());
for (long i = 1; i <= 3000; i += 3) { 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, TEMP, i, Math.min(i + 3, 3000), getTsForReadTsKvQuery(i, i + 3), COUNT);
verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, querySub.getKey(), i, i + 3, 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 --> <spring-test-dbunit.version>1.3.0</spring-test-dbunit.version> <!-- 2016 -->
<takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 --> <takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 -->
<!-- BLACKBOX TEST SCOPE --> <!-- BLACKBOX TEST SCOPE -->
<testcontainers.version>1.16.0</testcontainers.version> <testcontainers.version>1.17.3</testcontainers.version>
<zeroturnaround.version>1.12</zeroturnaround.version> <zeroturnaround.version>1.12</zeroturnaround.version>
<opensmpp.version>3.0.0</opensmpp.version> <opensmpp.version>3.0.0</opensmpp.version>
<jgit.version>6.1.0.202203080745-r</jgit.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. /// 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 { import {
AggregationType, AggregationType,
calculateIntervalComparisonEndTime, calculateIntervalComparisonEndTime,
@ -25,10 +28,10 @@ import {
SubscriptionTimewindow SubscriptionTimewindow
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { UtilsService } from '@core/services/utils.service'; 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; import Timeout = NodeJS.Timeout;
export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void; export declare type onAggregatedData = (data: IndexedSubscriptionData, detectChanges: boolean) => void;
interface AggData { interface AggData {
count: number; count: number;
@ -67,12 +70,12 @@ class AggDataMap {
} }
class AggregationMap { class AggregationMap {
aggMap: {[key: string]: AggDataMap} = {}; aggMap: {[id: number]: AggDataMap} = {};
detectRangeChanged(): boolean { detectRangeChanged(): boolean {
let changed = false; let changed = false;
for (const key of Object.keys(this.aggMap)) { for (const id of Object.keys(this.aggMap)) {
const aggDataMap = this.aggMap[key]; const aggDataMap = this.aggMap[id];
if (aggDataMap.rangeChanged) { if (aggDataMap.rangeChanged) {
changed = true; changed = true;
aggDataMap.rangeChanged = false; aggDataMap.rangeChanged = false;
@ -82,8 +85,8 @@ class AggregationMap {
} }
clearRangeChangedFlags() { clearRangeChangedFlags() {
for (const key of Object.keys(this.aggMap)) { for (const id of Object.keys(this.aggMap)) {
this.aggMap[key].rangeChanged = false; this.aggMap[id].rangeChanged = false;
} }
} }
} }
@ -93,7 +96,7 @@ declare type AggFunction = (aggData: AggData, value?: any) => void;
const avg: AggFunction = (aggData: AggData, value?: any) => { const avg: AggFunction = (aggData: AggData, value?: any) => {
aggData.count++; aggData.count++;
if (isNumber(value)) { if (isNumber(value)) {
aggData.sum += value; aggData.sum = aggData.aggValue * (aggData.count - 1) + value;
aggData.aggValue = aggData.sum / aggData.count; aggData.aggValue = aggData.sum / aggData.count;
} else { } else {
aggData.aggValue = value; aggData.aggValue = value;
@ -135,9 +138,25 @@ const none: AggFunction = (aggData: AggData, value?: any) => {
export class DataAggregator { export class DataAggregator {
private dataBuffer: SubscriptionData = {}; constructor(private onDataCb: onAggregatedData,
private data: SubscriptionData; private tsKeys: AggKey[],
private readonly lastPrevKvPairData: {[key: string]: [number, any]}; 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; private aggregationMap: AggregationMap;
@ -145,9 +164,7 @@ export class DataAggregator {
private resetPending = false; private resetPending = false;
private updatedData = false; private updatedData = false;
private noAggregation = this.subsTw.aggregation.type === AggregationType.NONE; private aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(this.subsTw.aggregation.interval, 1000);
private aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000);
private readonly aggFunction: AggFunction;
private intervalTimeoutHandle: Timeout; private intervalTimeoutHandle: Timeout;
private intervalScheduledTime: number; private intervalScheduledTime: number;
@ -156,38 +173,29 @@ export class DataAggregator {
private endTs: number; private endTs: number;
private elapsed: number; private elapsed: number;
constructor(private onDataCb: onAggregatedData, private static convertValue(val: string, noAggregation: boolean): any {
private tsKeyNames: string[], if (val && isNumeric(val) && (!noAggregation || noAggregation && Number(val).toString() === val)) {
private subsTw: SubscriptionTimewindow, return Number(val);
private utils: UtilsService,
private ignoreDataUpdateOnIntervalTick: boolean) {
this.tsKeyNames.forEach((key) => {
this.dataBuffer[key] = [];
});
if (this.subsTw.aggregation.stateData) {
this.lastPrevKvPairData = {};
} }
switch (this.subsTw.aggregation.type) { return val;
}
private static getAggFunction(aggType: AggregationType): AggFunction {
switch (aggType) {
case AggregationType.MIN: case AggregationType.MIN:
this.aggFunction = min; return min;
break;
case AggregationType.MAX: case AggregationType.MAX:
this.aggFunction = max; return max;
break;
case AggregationType.AVG: case AggregationType.AVG:
this.aggFunction = avg; return avg;
break;
case AggregationType.SUM: case AggregationType.SUM:
this.aggFunction = sum; return sum;
break;
case AggregationType.COUNT: case AggregationType.COUNT:
this.aggFunction = count; return count;
break;
case AggregationType.NONE: case AggregationType.NONE:
this.aggFunction = none; return none;
break;
default: default:
this.aggFunction = avg; return avg;
} }
} }
@ -206,7 +214,7 @@ export class DataAggregator {
this.intervalScheduledTime = this.utils.currentPerfTime(); this.intervalScheduledTime = this.utils.currentPerfTime();
this.calculateStartEndTs(); this.calculateStartEndTs();
this.elapsed = 0; 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.resetPending = true;
this.updatedData = false; this.updatedData = false;
this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout);
@ -220,7 +228,7 @@ export class DataAggregator {
this.aggregationMap = null; 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; this.updatedData = true;
if (!this.dataReceived || this.resetPending) { if (!this.dataReceived || this.resetPending) {
let updateIntervalScheduledTime = true; let updateIntervalScheduledTime = true;
@ -235,9 +243,9 @@ export class DataAggregator {
} }
if (update) { if (update) {
this.aggregationMap = new AggregationMap(); this.aggregationMap = new AggregationMap();
this.updateAggregatedData(data.data); this.updateAggregatedData(data);
} else { } else {
this.aggregationMap = this.processAggregatedData(data.data); this.aggregationMap = this.processAggregatedData(data);
} }
if (updateIntervalScheduledTime) { if (updateIntervalScheduledTime) {
this.intervalScheduledTime = this.utils.currentPerfTime(); this.intervalScheduledTime = this.utils.currentPerfTime();
@ -245,7 +253,7 @@ export class DataAggregator {
this.aggregationMap.clearRangeChangedFlags(); this.aggregationMap.clearRangeChangedFlags();
this.onInterval(history, detectChanges); this.onInterval(history, detectChanges);
} else { } else {
this.updateAggregatedData(data.data); this.updateAggregatedData(data);
if (history) { if (history) {
this.intervalScheduledTime = this.utils.currentPerfTime(); this.intervalScheduledTime = this.utils.currentPerfTime();
this.onInterval(history, detectChanges); this.onInterval(history, detectChanges);
@ -283,9 +291,9 @@ export class DataAggregator {
} }
const intervalTimeout = rangeChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout; const intervalTimeout = rangeChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout;
if (!history) { 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) { if (delta || !this.data || rangeChanged) {
const tickTs = delta * this.subsTw.aggregation.interval; const tickTs = delta * this.aggregationTimeout;
if (this.subsTw.quickInterval) { if (this.subsTw.quickInterval) {
const startEndTime = calculateIntervalStartEndTime(this.subsTw.quickInterval, this.subsTw.timezone); const startEndTime = calculateIntervalStartEndTime(this.subsTw.quickInterval, this.subsTw.timezone);
this.startTs = startEndTime[0] + this.subsTw.tsOffset; this.startTs = startEndTime[0] + this.subsTw.tsOffset;
@ -295,7 +303,7 @@ export class DataAggregator {
this.endTs += tickTs; this.endTs += tickTs;
} }
this.data = this.updateData(); this.data = this.updateData();
this.elapsed = this.elapsed - delta * this.subsTw.aggregation.interval; this.elapsed = this.elapsed - delta * this.aggregationTimeout;
} }
} else { } else {
this.data = this.updateData(); this.data = this.updateData();
@ -309,39 +317,45 @@ export class DataAggregator {
} }
} }
private updateData(): SubscriptionData { private updateData(): IndexedSubscriptionData {
this.tsKeyNames.forEach((key) => { this.dataBuffer = [];
this.dataBuffer[key] = []; this.tsKeys.forEach((key) => {
if (!this.dataBuffer[key.id]) {
this.dataBuffer[key.id] = [];
}
}); });
for (const key of Object.keys(this.aggregationMap.aggMap)) { for (const idStr of Object.keys(this.aggregationMap.aggMap)) {
const aggKeyData = this.aggregationMap.aggMap[key]; const id = Number(idStr);
let keyData = this.dataBuffer[key]; 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) => { aggKeyData.forEach((aggData, aggTimestamp) => {
if (aggTimestamp < this.startTs) { if (aggTimestamp < this.startTs) {
if (this.subsTw.aggregation.stateData && if (this.subsTw.aggregation.stateData &&
(!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { (!this.lastPrevKvPairData[id] || this.lastPrevKvPairData[id][0] < aggTimestamp)) {
this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; this.lastPrevKvPairData[id] = [aggTimestamp, aggData.aggValue];
} }
aggKeyData.delete(aggTimestamp); aggKeyData.delete(aggTimestamp);
this.updatedData = true; this.updatedData = true;
} else if (aggTimestamp < this.endTs || this.noAggregation) { } else if (aggTimestamp < this.endTs || noAggregation) {
const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; const kvPair: [number, any] = [aggTimestamp, aggData.aggValue];
keyData.push(kvPair); keyData.push(kvPair);
} }
}); });
keyData.sort((set1, set2) => set1[0] - set2[0]); keyData.sort((set1, set2) => set1[0] - set2[0]);
if (this.subsTw.aggregation.stateData) { 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) { if (keyData.length > this.subsTw.aggregation.limit) {
keyData = keyData.slice(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; return this.dataBuffer;
} }
private updateStateBounds(keyData: [number, any][], lastPrevKvPair: [number, any]) { private updateStateBounds(keyData: [number, any, number?][], lastPrevKvPair: [number, any]) {
if (lastPrevKvPair) { if (lastPrevKvPair) {
lastPrevKvPair[0] = this.startTs; lastPrevKvPair[0] = this.startTs;
} }
@ -369,66 +383,71 @@ export class DataAggregator {
} }
} }
private processAggregatedData(data: SubscriptionData): AggregationMap { private processAggregatedData(data: IndexedSubscriptionData): AggregationMap {
const isCount = this.subsTw.aggregation.type === AggregationType.COUNT;
const aggregationMap = new AggregationMap(); const aggregationMap = new AggregationMap();
for (const key of Object.keys(data)) { for (const idStr of Object.keys(data)) {
let aggKeyData = aggregationMap.aggMap[key]; 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) { if (!aggKeyData) {
aggKeyData = new AggDataMap(); aggKeyData = new AggDataMap();
aggregationMap.aggMap[key] = aggKeyData; aggregationMap.aggMap[id] = aggKeyData;
} }
const keyData = data[key]; const keyData = data[id];
keyData.forEach((kvPair) => { keyData.forEach((kvPair) => {
const timestamp = kvPair[0]; const timestamp = kvPair[0];
const value = this.convertValue(kvPair[1]); const value = DataAggregator.convertValue(kvPair[1], noAggregation);
const aggKey = timestamp; const tsKey = timestamp;
const aggData = { const aggData = {
count: isCount ? value : 1, count: isCount ? value : isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1,
sum: value, sum: value,
aggValue: value aggValue: value
}; };
aggKeyData.set(aggKey, aggData); aggKeyData.set(tsKey, aggData);
}); });
} }
return aggregationMap; return aggregationMap;
} }
private updateAggregatedData(data: SubscriptionData) { private updateAggregatedData(data: IndexedSubscriptionData) {
const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; for (const idStr of Object.keys(data)) {
for (const key of Object.keys(data)) { const id = Number(idStr);
let aggKeyData = this.aggregationMap.aggMap[key]; 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) { if (!aggKeyData) {
aggKeyData = new AggDataMap(); aggKeyData = new AggDataMap();
this.aggregationMap.aggMap[key] = aggKeyData; this.aggregationMap.aggMap[id] = aggKeyData;
} }
const keyData = data[key]; const keyData = data[id];
keyData.forEach((kvPair) => { keyData.forEach((kvPair) => {
const timestamp = kvPair[0]; const timestamp = kvPair[0];
const value = this.convertValue(kvPair[1]); const value = DataAggregator.convertValue(kvPair[1], noAggregation);
const aggTimestamp = this.noAggregation ? timestamp : (this.startTs + const aggTimestamp = noAggregation ? timestamp : (this.startTs +
Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) * Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) *
this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2); this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2);
let aggData = aggKeyData.get(aggTimestamp); let aggData = aggKeyData.get(aggTimestamp);
if (!aggData) { if (!aggData) {
aggData = { aggData = {
count: 1, count: isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1,
sum: value, sum: value,
aggValue: isCount ? 1 : value aggValue: isCount ? 1 : value
}; };
aggKeyData.set(aggTimestamp, aggData); aggKeyData.set(aggTimestamp, aggData);
} else { } else {
this.aggFunction(aggData, value); DataAggregator.getAggFunction(aggType)(aggData, value);
} }
}); });
} }
} }
private convertValue(val: string): any { private aggKeyById(id: number): AggKey {
if (val && isNumeric(val) && (!this.noAggregation || this.noAggregation && Number(val).toString() === val)) { return this.tsKeys.find(key => key.id === id);
return Number(val);
}
return val;
} }
} }

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

@ -14,9 +14,16 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; import { ComparisonResultType, DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models';
import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models';
import { import {
AggregationType,
ComparisonDuration,
createTimewindowForComparison,
getCurrentTime,
SubscriptionTimewindow
} from '@shared/models/time/time.models';
import {
ComparisonTsValue,
EntityData, EntityData,
EntityDataPageLink, EntityDataPageLink,
EntityFilter, EntityFilter,
@ -28,11 +35,12 @@ import {
TsValue TsValue
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { import {
AggKey,
DataKeyType, DataKeyType,
EntityCountCmd, EntityCountCmd,
EntityDataCmd, EntityDataCmd,
IndexedSubscriptionData,
SubscriptionData, SubscriptionData,
SubscriptionDataHolder,
TelemetryService, TelemetryService,
TelemetrySubscriber TelemetrySubscriber
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
@ -55,6 +63,11 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number,
export interface SubscriptionDataKey { export interface SubscriptionDataKey {
name: string; name: string;
type: DataKeyType; type: DataKeyType;
aggregationType?: AggregationType;
comparisonEnabled?: boolean;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
comparisonResultType?: ComparisonResultType;
funcBody: string; funcBody: string;
func?: DataKeyFunction; func?: DataKeyFunction;
postFuncBody: string; postFuncBody: string;
@ -82,9 +95,16 @@ export interface EntityDataSubscriptionOptions {
export class EntityDataSubscription { export class EntityDataSubscription {
constructor(private listener: EntityDataListener,
private telemetryService: TelemetryService,
private utils: UtilsService) {
this.initializeSubscription();
}
private entityDataSubscriptionOptions = this.listener.subscriptionOptions; private entityDataSubscriptionOptions = this.listener.subscriptionOptions;
private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType; private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType;
private history: boolean; private history: boolean;
private isFloatingTimewindow: boolean;
private realtime: boolean; private realtime: boolean;
private subscriber: TelemetrySubscriber; private subscriber: TelemetrySubscriber;
@ -95,13 +115,18 @@ export class EntityDataSubscription {
private attrFields: Array<EntityKey>; private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>; private tsFields: Array<EntityKey>;
private latestValues: Array<EntityKey>; private latestValues: Array<EntityKey>;
private aggTsValues: Array<AggKey>;
private aggTsComparisonValues: Array<AggKey>;
private entityDataResolveSubject: Subject<EntityDataLoadResult>; private entityDataResolveSubject: Subject<EntityDataLoadResult>;
private pageData: PageData<EntityData>; private pageData: PageData<EntityData>;
private data: Array<Array<DataSetHolder>>;
private subsTw: SubscriptionTimewindow; private subsTw: SubscriptionTimewindow;
private latestTsOffset: number; private latestTsOffset: number;
private dataAggregators: Array<DataAggregator>; private dataAggregators: Array<DataAggregator>;
private tsLatestDataAggregators: Array<DataAggregator>;
private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {}; private dataKeys: {[key: string]: Array<SubscriptionDataKey> | SubscriptionDataKey} = {};
private dataKeysList: SubscriptionDataKey[] = [];
private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceData: {[index: number]: {[key: string]: DataSetHolder}};
private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}};
private entityIdToDataIndex: {[id: string]: number}; private entityIdToDataIndex: {[id: string]: number};
@ -116,15 +141,44 @@ export class EntityDataSubscription {
private dataResolved = false; private dataResolved = false;
private started = false; private started = false;
constructor(private listener: EntityDataListener, private static convertValue(val: string): any {
private telemetryService: TelemetryService, if (val && isNumeric(val) && Number(val).toString() === val) {
private utils: UtilsService) { return Number(val);
this.initializeSubscription(); }
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() { private initializeSubscription() {
for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) { for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) {
const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]); const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]);
this.dataKeysList.push(dataKey);
dataKey.index = i; dataKey.index = i;
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
if (!dataKey.func) { if (!dataKey.func) {
@ -142,7 +196,8 @@ export class EntityDataSubscription {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
} else { } 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>; let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
if (!dataKeysList) { if (!dataKeysList) {
@ -180,6 +235,12 @@ export class EntityDataSubscription {
}); });
this.dataAggregators = null; this.dataAggregators = null;
} }
if (this.tsLatestDataAggregators) {
this.tsLatestDataAggregators.forEach((aggregator) => {
aggregator.destroy();
});
this.tsLatestDataAggregators = null;
}
this.pageData = null; this.pageData = null;
} }
@ -188,16 +249,11 @@ export class EntityDataSubscription {
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.started = true; this.started = true;
this.dataResolved = true; this.dataResolved = true;
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; this.prepareSubscriptionTimewindow();
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);
} }
if (this.datasourceType === DatasourceType.entity) { if (this.datasourceType === DatasourceType.entity) {
const entityFields: Array<EntityKey> = 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 }) dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name })
); );
if (!entityFields.find(key => key.key === '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 }) dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
); );
this.tsFields = this.entityDataSubscriptionOptions.dataKeys. this.tsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && !dataKey.latest).map( filter(dataKey => dataKey.type === DataKeyType.timeseries &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
); );
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const latestTsFields = this.entityDataSubscriptionOptions.dataKeys. const latestTsFields = this.dataKeysList.
filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest).map( filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest &&
(!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
); );
this.latestValues = this.attrFields.concat(latestTsFields); this.latestValues = this.attrFields.concat(latestTsFields);
@ -238,6 +296,19 @@ export class EntityDataSubscription {
this.latestValues = this.attrFields.concat(this.tsFields); 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.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd(); this.dataCommand = new EntityDataCmd();
@ -282,19 +353,28 @@ export class EntityDataSubscription {
this.subscriber.reconnect$.subscribe(() => { this.subscriber.reconnect$.subscribe(() => {
if (this.started) { if (this.started) {
const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length ||
!this.history && this.tsFields.length) { this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) {
const newSubsTw = this.listener.updateRealtimeSubscription(); const newSubsTw = this.listener.updateRealtimeSubscription();
this.subsTw = newSubsTw; this.subsTw = newSubsTw;
targetCommand.tsCmd.startTs = this.subsTw.startTs; if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) {
targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; targetCommand.tsCmd.startTs = this.subsTw.startTs;
targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow;
targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; targetCommand.tsCmd.interval = this.subsTw.aggregation.interval;
targetCommand.tsCmd.agg = this.subsTw.aggregation.type; targetCommand.tsCmd.limit = this.subsTw.aggregation.limit;
targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; targetCommand.tsCmd.agg = this.subsTw.aggregation.type;
this.dataAggregators.forEach((dataAggregator) => { targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData;
dataAggregator.reset(newSubsTw); 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) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.subscriber.setTsOffset(this.subsTw.tsOffset); this.subscriber.setTsOffset(this.subsTw.tsOffset);
@ -359,7 +439,7 @@ export class EntityDataSubscription {
entityType: null entityType: null
}; };
const countKey = this.entityDataSubscriptionOptions.dataKeys[0]; const countKey = this.dataKeysList[0];
let dataReceived = false; let dataReceived = false;
@ -422,14 +502,9 @@ export class EntityDataSubscription {
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return; return;
} }
this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; this.prepareSubscriptionTimewindow();
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.prepareData(); this.prepareData(true);
if (this.datasourceType === DatasourceType.entity) { if (this.datasourceType === DatasourceType.entity) {
this.subsCommand = new EntityDataCmd(); this.subsCommand = new EntityDataCmd();
@ -461,7 +536,19 @@ export class EntityDataSubscription {
this.started = true; 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) { private prepareSubscriptionCommands(cmd: EntityDataCmd) {
let latestValuesKeys: EntityKey[] = [];
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.tsFields.length > 0) { if (this.tsFields.length > 0) {
if (this.history) { if (this.history) {
@ -486,17 +573,40 @@ export class EntityDataSubscription {
}; };
} }
} }
if (this.latestValues.length > 0) { latestValuesKeys = this.latestValues;
cmd.latestCmd = {
keys: this.latestValues
};
}
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.latestValues.length > 0) { latestValuesKeys = this.latestValues;
cmd.latestCmd = { }
keys: 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); this.generateData(true);
} }
private prepareData() { private prepareData(isUpdate: boolean) {
if (this.timeseriesTimer) { if (this.timeseriesTimer) {
clearTimeout(this.timeseriesTimer); clearTimeout(this.timeseriesTimer);
this.timeseriesTimer = null; this.timeseriesTimer = null;
@ -526,37 +636,73 @@ export class EntityDataSubscription {
}); });
} }
this.dataAggregators = []; this.dataAggregators = [];
if (this.tsLatestDataAggregators) {
this.tsLatestDataAggregators.forEach((aggregator) => {
aggregator.destroy();
});
}
this.tsLatestDataAggregators = [];
this.resetData(); this.resetData();
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
let tsKeyNames = []; let tsKeyIds: number[];
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
for (const key of Object.keys(this.dataKeys)) { tsKeyIds = this.dataKeysList.filter(key => !key.latest).map(key => key.index);
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeysList.forEach((subscriptionDataKey) => {
if (!subscriptionDataKey.latest) {
tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`);
}
});
}
} else { } 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++) { const aggKeys: AggKey[] = tsKeyIds.map(key => ({id: key, key: key + '', agg: this.subsTw.aggregation.type}));
if (tsKeyNames.length) { if (aggKeys.length) {
if (this.datasourceType === DatasourceType.function) { for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys,
DataKeyType.function, dataIndex, this.notifyListener.bind(this)); false, dataIndex, this.notifyListener.bind(this));
} else {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.timeseries, 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() { private resetData() {
this.data = [];
this.datasourceData = []; this.datasourceData = [];
this.entityIdToDataIndex = {}; this.entityIdToDataIndex = {};
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
@ -609,19 +755,18 @@ export class EntityDataSubscription {
this.pageData = pageData; this.pageData = pageData;
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.prepareData(); this.prepareData(false);
} else if (isInitialData) { } else if (isInitialData) {
this.resetData(); this.resetData();
} }
const data: Array<Array<DataSetHolder>> = [];
for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) {
const entityData = pageData.data[dataIndex]; const entityData = pageData.data[dataIndex];
this.processEntityData(entityData, dataIndex, false, this.processEntityData(entityData, dataIndex, false,
(data1, dataIndex1, dataKeyIndex) => { (data1, dataIndex1, dataKeyIndex) => {
if (!data[dataIndex1]) { if (!this.data[dataIndex1]) {
data[dataIndex1] = []; this.data[dataIndex1] = [];
} }
data[dataIndex1][dataKeyIndex] = data1; this.data[dataIndex1][dataKeyIndex] = data1;
} }
); );
} }
@ -630,7 +775,7 @@ export class EntityDataSubscription {
this.entityDataResolveSubject.next( this.entityDataResolveSubject.next(
{ {
pageData, pageData,
data, data: this.data,
datasourceIndex: this.listener.configDatasourceIndex, datasourceIndex: this.listener.configDatasourceIndex,
pageLink: this.entityDataSubscriptionOptions.pageLink pageLink: this.entityDataSubscriptionOptions.pageLink
} }
@ -638,7 +783,7 @@ export class EntityDataSubscription {
this.entityDataResolveSubject.complete(); this.entityDataResolveSubject.complete();
} else { } else {
if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.listener.dataLoaded(pageData, data, this.listener.dataLoaded(pageData, this.data,
this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink); this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink);
} }
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) {
@ -648,7 +793,7 @@ export class EntityDataSubscription {
this.entityDataResolveSubject.next( this.entityDataResolveSubject.next(
{ {
pageData, pageData,
data, data: this.data,
datasourceIndex: this.listener.configDatasourceIndex, datasourceIndex: this.listener.configDatasourceIndex,
pageLink: this.entityDataSubscriptionOptions.pageLink pageLink: this.entityDataSubscriptionOptions.pageLink
} }
@ -675,27 +820,85 @@ export class EntityDataSubscription {
private processEntityData(entityData: EntityData, dataIndex: number, isUpdate: boolean, private processEntityData(entityData: EntityData, dataIndex: number, isUpdate: boolean,
dataUpdatedCb: DataUpdatedCb) { dataUpdatedCb: DataUpdatedCb) {
if ((this.entityDataSubscriptionOptions.type === widgetType.latest || if (this.entityDataSubscriptionOptions.type === widgetType.latest ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) && entityData.latest) { this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
for (const type of Object.keys(entityData.latest)) { if (entityData.aggLatest) {
const subscriptionData = this.toSubscriptionData(entityData.latest[type], false); const aggData: IndexedSubscriptionData = [];
const dataKeyType = entityKeyTypeToDataKeyType(EntityKeyType[type]); for (const idStr of Object.keys(entityData.aggLatest)) {
this.onData(subscriptionData, dataKeyType, dataIndex, true, const id = Number(idStr);
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); 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) { if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && entityData.timeseries) {
const subscriptionData = this.toSubscriptionData(entityData.timeseries, true); const subscriptionData = this.toSubscriptionData(entityData.timeseries, true);
if (this.dataAggregators && this.dataAggregators[dataIndex]) { if (this.dataAggregators && this.dataAggregators[dataIndex]) {
const dataAggregator = 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; let prevDataCb;
if (!isUpdate) { if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => { prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onData(data, this.datasourceType === DatasourceType.function ? this.onIndexedData(data, dataIndex, detectChanges, false, dataUpdatedCb);
DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, false, dataUpdatedCb);
}); });
} }
dataAggregator.onData({data: subscriptionData}, false, this.history, true); dataAggregator.onData(indexedData, false, this.history, true);
if (prevDataCb) { if (prevDataCb) {
dataAggregator.updateOnDataCb(prevDataCb); dataAggregator.updateOnDataCb(prevDataCb);
} }
@ -707,92 +910,109 @@ export class EntityDataSubscription {
private onData(sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean, private onData(sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean,
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) { isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const keyName of Object.keys(sourceData)) { for (const key of Object.keys(sourceData)) {
const keyData = sourceData[keyName]; const keyData = sourceData[key];
const key = `${keyName}_${type}${isTsLatest ? '_latest' : ''}`; this.onKeyData(keyData, key, 0, type,
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>; dataIndex, detectChanges, isTsLatest, false, dataUpdatedCb);
for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { }
const datasourceKey = `${key}_${keyIndex}`; }
if (this.datasourceData[dataIndex][datasourceKey].data) {
const dataKey = dataKeyList[keyIndex]; private onIndexedData(sourceData: IndexedSubscriptionData, dataIndex: number, detectChanges: boolean,
const data: DataSet = []; isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
let prevSeries: [number, any]; for (const indexStr of Object.keys(sourceData)) {
let prevOrigSeries: [number, any]; const id = Number(indexStr);
let datasourceKeyData: DataSet; const dataKey = this.dataKeyByIndex(id);
let datasourceOrigKeyData: DataSet; const isAggLatest = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE;
let update = false; const keyData = sourceData[id];
if (this.realtime && !isTsLatest) { let keyName = dataKey.name;
datasourceKeyData = []; if (dataKey.type === DataKeyType.function) {
datasourceOrigKeyData = []; keyName += `_${dataKey.index}`;
} 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);
}
}
} }
this.onKeyData(keyData, keyName, id, dataKey.type,
dataIndex, detectChanges, isTsLatest, isAggLatest, dataUpdatedCb);
} }
} }
private convertValue(val: string): any { private onKeyData(keyData: [number, any, number?][], keyName: string, id: number, type: DataKeyType,
if (val && isNumeric(val) && Number(val).toString() === val) { dataIndex: number, detectChanges: boolean,
return Number(val); 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 { private toSubscriptionData(sourceData: {[key: string]: TsValue | TsValue[]}, isTs: boolean): SubscriptionData {
const subsData: SubscriptionData = {}; const subsData: SubscriptionData = {};
for (const keyName of Object.keys(sourceData)) { for (const keyName of Object.keys(sourceData)) {
const values = sourceData[keyName]; const values = sourceData[keyName];
const dataSet: [number, any][] = []; const dataSet: [number, any, number?][] = [];
if (isTs) { if (isTs) {
(values as TsValue[]).forEach((keySeries) => { (values as TsValue[]).forEach((keySeries) => {
dataSet.push([keySeries.ts, keySeries.value]); dataSet.push([keySeries.ts, keySeries.value, keySeries.count]);
}); });
} else { } else {
const tsValue = values as TsValue; const tsValue = values as TsValue;
dataSet.push([tsValue.ts, tsValue.value]); dataSet.push([tsValue.ts, tsValue.value, tsValue.count]);
} }
subsData[keyName] = dataSet; subsData[keyName] = dataSet;
} }
@ -800,21 +1020,37 @@ export class EntityDataSubscription {
} }
private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow, private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow,
tsKeyNames: Array<string>, tsKeys: Array<AggKey>,
dataKeyType: DataKeyType, isLatestDataAgg: boolean,
dataIndex: number, dataIndex: number,
dataUpdatedCb: DataUpdatedCb): DataAggregator { dataUpdatedCb: DataUpdatedCb): DataAggregator {
return new DataAggregator( return new DataAggregator(
(data, detectChanges) => { (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, subsTw,
this.utils, 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][] { private generateSeries(dataKey: SubscriptionDataKey, startTime: number, endTime: number): [number, any][] {
const data: [number, any][] = []; const data: [number, any][] = [];
let prevSeries: [number, any]; let prevSeries: [number, any];
@ -893,9 +1129,7 @@ export class EntityDataSubscription {
let startTime: number; let startTime: number;
let endTime: number; let endTime: number;
let delta: number; let delta: number;
const generatedData: SubscriptionDataHolder = { const generatedData: IndexedSubscriptionData = [];
data: {}
};
if (!this.history) { if (!this.history) {
delta = Math.floor(this.tickElapsed / this.frequency); delta = Math.floor(this.tickElapsed / this.frequency);
} }
@ -928,7 +1162,7 @@ export class EntityDataSubscription {
endTime = Math.min(currentTime, endTime); 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) { if (this.dataAggregators && this.dataAggregators.length) {
this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges); 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 { export interface EntityDataListener {
subscriptionType: widgetType; subscriptionType: widgetType;
useTimewindow?: boolean;
subscriptionTimewindow?: SubscriptionTimewindow; subscriptionTimewindow?: SubscriptionTimewindow;
latestTsOffset?: number; latestTsOffset?: number;
configDatasource: Datasource; 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, public prepareSubscription(listener: EntityDataListener,
ignoreDataUpdateOnIntervalTick = false): Observable<EntityDataLoadResult> { ignoreDataUpdateOnIntervalTick = false): Observable<EntityDataLoadResult> {
const datasource = listener.configDatasource; const datasource = listener.configDatasource;
@ -93,10 +109,10 @@ export class EntityDataService {
public startSubscription(listener: EntityDataListener) { public startSubscription(listener: EntityDataListener) {
if (listener.subscription) { if (listener.subscription) {
if (listener.subscriptionType === widgetType.timeseries) { if (listener.useTimewindow) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); 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.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} }
listener.subscription.start(); listener.subscription.start();
@ -122,10 +138,10 @@ export class EntityDataService {
return of(null); return of(null);
} }
listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); 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.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.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} }
return listener.subscription.subscribe(); return listener.subscription.subscribe();
@ -146,11 +162,11 @@ export class EntityDataService {
ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions { ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions {
const subscriptionDataKeys: Array<SubscriptionDataKey> = []; const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
datasource.dataKeys.forEach((dataKey) => { datasource.dataKeys.forEach((dataKey) => {
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, false)); subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, false));
}); });
if (datasource.latestDataKeys) { if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => { datasource.latestDataKeys.forEach((dataKey) => {
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, true)); subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, true));
}); });
} }
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = { const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
@ -171,14 +187,4 @@ export class EntityDataService {
entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick; entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick;
return entityDataSubscriptionOptions; 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, DataSetHolder,
Datasource, Datasource,
DatasourceData, DatasourceData,
datasourcesHasAggregation,
DatasourceType, DatasourceType,
LegendConfig, LegendConfig,
LegendData, LegendData,
@ -95,6 +96,7 @@ export class WidgetSubscription implements IWidgetSubscription {
timezone: string; timezone: string;
subscriptionTimewindow: SubscriptionTimewindow; subscriptionTimewindow: SubscriptionTimewindow;
useDashboardTimewindow: boolean; useDashboardTimewindow: boolean;
useTimewindow: boolean;
tsOffset = 0; tsOffset = 0;
hasDataPageLink: boolean; hasDataPageLink: boolean;
@ -200,6 +202,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.originalTimewindow = null; this.originalTimewindow = null;
this.timeWindow = {}; this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow; this.useDashboardTimewindow = options.useDashboardTimewindow;
this.useTimewindow = true;
if (this.useDashboardTimewindow) { if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow); this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else { } else {
@ -245,15 +248,16 @@ export class WidgetSubscription implements IWidgetSubscription {
this.timeWindow = {}; this.timeWindow = {};
this.useDashboardTimewindow = options.useDashboardTimewindow; this.useDashboardTimewindow = options.useDashboardTimewindow;
this.stateData = options.stateData; this.stateData = options.stateData;
if (this.type === widgetType.latest) { this.useTimewindow = this.type === widgetType.timeseries || datasourcesHasAggregation(this.configuredDatasources);
this.timezone = options.dashboardTimewindow.timezone;
this.updateTsOffset();
}
if (this.useDashboardTimewindow) { if (this.useDashboardTimewindow) {
this.timeWindowConfig = deepClone(options.dashboardTimewindow); this.timeWindowConfig = deepClone(options.dashboardTimewindow);
} else { } else {
this.timeWindowConfig = deepClone(options.timeWindowConfig); 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.subscriptionTimewindow = null;
this.comparisonEnabled = options.comparisonEnabled && isHistoryTypeTimewindow(this.timeWindowConfig); this.comparisonEnabled = options.comparisonEnabled && isHistoryTypeTimewindow(this.timeWindowConfig);
@ -443,6 +447,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const resolveResultObservables = this.configuredDatasources.map((datasource, index) => { const resolveResultObservables = this.configuredDatasources.map((datasource, index) => {
const listener: EntityDataListener = { const listener: EntityDataListener = {
subscriptionType: this.type, subscriptionType: this.type,
useTimewindow: this.useTimewindow,
configDatasource: datasource, configDatasource: datasource,
configDatasourceIndex: index, configDatasourceIndex: index,
dataLoaded: (pageData, data1, datasourceIndex, pageLink) => { dataLoaded: (pageData, data1, datasourceIndex, pageLink) => {
@ -626,22 +631,31 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) { 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.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) { if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) {
const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow); isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow);
this.timeWindowConfig = deepClone(newDashboardTimewindow); this.timeWindowConfig = deepClone(newDashboardTimewindow);
this.update(isTimewindowTypeChanged); doUpdate = true;
} }
} }
} else if (this.type === widgetType.latest) { } else if (this.type === widgetType.latest) {
if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) { if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) {
this.timezone = newDashboardTimewindow.timezone; this.timezone = newDashboardTimewindow.timezone;
if (this.updateTsOffset()) { doUpdate = this.updateTsOffset();
this.update();
}
} }
} }
if (doUpdate) {
this.update(isTimewindowTypeChanged);
}
} }
updateDataVisibility(index: number): void { updateDataVisibility(index: number): void {
@ -660,6 +674,12 @@ export class WidgetSubscription implements IWidgetSubscription {
updateTimewindowConfig(newTimewindow: Timewindow): void { updateTimewindowConfig(newTimewindow: Timewindow): void {
if (!this.useDashboardTimewindow) { 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); const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newTimewindow);
this.timeWindowConfig = newTimewindow; this.timeWindowConfig = newTimewindow;
this.update(isTimewindowTypeChanged); this.update(isTimewindowTypeChanged);
@ -874,11 +894,12 @@ export class WidgetSubscription implements IWidgetSubscription {
} }
const datasource = this.configuredDatasources[datasourceIndex]; const datasource = this.configuredDatasources[datasourceIndex];
if (datasource) { if (datasource) {
if (this.type === widgetType.timeseries && this.timeWindowConfig) { if (this.useTimewindow && this.timeWindowConfig) {
this.updateRealtimeSubscription(); this.updateRealtimeSubscription();
} }
entityDataListener = { entityDataListener = {
subscriptionType: this.type, subscriptionType: this.type,
useTimewindow: this.useTimewindow,
configDatasource: datasource, configDatasource: datasource,
configDatasourceIndex: datasourceIndex, configDatasourceIndex: datasourceIndex,
subscriptionTimewindow: this.subscriptionTimewindow, subscriptionTimewindow: this.subscriptionTimewindow,
@ -940,7 +961,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateDataTimewindow() { private updateDataTimewindow() {
if (!this.hasDataPageLink) { if (!this.hasDataPageLink) {
if (this.type === widgetType.timeseries && this.timeWindowConfig) { if (this.useTimewindow && this.timeWindowConfig) {
this.updateRealtimeSubscription(); this.updateRealtimeSubscription();
if (this.comparisonEnabled) { if (this.comparisonEnabled) {
this.updateSubscriptionForComparison(); this.updateSubscriptionForComparison();
@ -952,11 +973,11 @@ export class WidgetSubscription implements IWidgetSubscription {
private dataSubscribe() { private dataSubscribe() {
this.updateDataTimewindow(); this.updateDataTimewindow();
if (!this.hasDataPageLink) { if (!this.hasDataPageLink) {
if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) { if (this.useTimewindow && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) {
this.onDataUpdated(); this.onDataUpdated();
} }
const forceUpdate = !this.datasources.length; 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) => { this.entityDataListeners.forEach((listener) => {
if (this.comparisonEnabled && listener.configDatasource.isAdditional) { if (this.comparisonEnabled && listener.configDatasource.isAdditional) {
listener.subscriptionTimewindow = this.timewindowForComparison; 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 { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -215,6 +216,9 @@ export class DashboardUtilsService {
delete datasource.deviceAliasId; delete datasource.deviceAliasId;
} }
}); });
if (widget.type === widgetType.latest) {
widget.config.timewindow = initModelFromDefaultTimewindow(widget.config.timewindow, true, this.timeService);
}
// Temp workaround // Temp workaround
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
widget.typeAlias = 'basic_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 || [] dataKeys: config.alarmSource.dataKeys || []
}; };
} }
const newWidget: Widget = { let newWidget: Widget = {
isSystemType: widget.isSystemType, isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias, bundleAlias: widget.bundleAlias,
typeAlias: widgetTypeInfo.alias, typeAlias: widgetTypeInfo.alias,
@ -1076,6 +1076,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
row: 0, row: 0,
col: 0 col: 0
}; };
newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget);
if (widgetTypeInfo.typeParameters.useCustomDatasources) { if (widgetTypeInfo.typeParameters.useCustomDatasources) {
this.addWidgetToDashboard(newWidget); this.addWidgetToDashboard(newWidget);
} else { } else {

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

@ -36,6 +36,7 @@
[dashboard]="data.dashboard" [dashboard]="data.dashboard"
[aliasController]="data.aliasController" [aliasController]="data.aliasController"
[widget]="data.widget" [widget]="data.widget"
[widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing" [showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks" [callbacks]="data.callbacks"
formControlName="dataKey"> 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 { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; 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 { DataKeysCallbacks } from './data-keys.component.models';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
@ -35,6 +35,7 @@ export interface DataKeyConfigDialogData {
dashboard: Dashboard; dashboard: Dashboard;
aliasController: IAliasController; aliasController: IAliasController;
widget: Widget; widget: Widget;
widgetType: widgetType;
entityAliasId?: string; entityAliasId?: string;
showPostProcessing?: boolean; showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks; 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"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</mat-form-field> </mat-form-field>
</div> </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"> <section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function">
<span translate>datakey.data-generation-func</span> <span translate>datakey.data-generation-func</span>
<br/> <br/>

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

@ -28,6 +28,36 @@
padding-left: 12px; 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 { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; 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 { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -43,6 +49,7 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models';
@Component({ @Component({
selector: 'tb-data-key-config', selector: 'tb-data-key-config',
@ -65,6 +72,22 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
dataKeyTypes = DataKeyType; 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() @Input()
entityAliasId: string; entityAliasId: string;
@ -80,6 +103,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input() @Input()
widget: Widget; widget: Widget;
@Input()
widgetType: widgetType;
@Input() @Input()
dataKeySettingsSchema: any; dataKeySettingsSchema: any;
@ -155,6 +181,11 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
} }
this.dataKeyFormGroup = this.fb.group({ this.dataKeyFormGroup = this.fb.group({
name: [null, []], 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]], label: [null, [Validators.required]],
color: [null, [Validators.required]], color: [null, [Validators.required]],
units: [null, []], units: [null, []],
@ -164,6 +195,32 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
postFuncBody: [null, []] 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.dataKeyFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });
@ -199,22 +256,86 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) {
this.modelValue.usePostProcessing = true; 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.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.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function &&
this.modelValue.type !== DataKeyType.count this.modelValue.type !== DataKeyType.count
? [Validators.required] : []); ? [Validators.required] : []);
if (this.modelValue.type === DataKeyType.count) { if (this.modelValue.type === DataKeyType.count) {
this.dataKeyFormGroup.get('name').disable({emitEvent: false}); this.dataKeyFormGroup.get('name').disable({emitEvent: false});
} else { } else {
this.dataKeyFormGroup.get('name').enable({emitEvent: false}); this.dataKeyFormGroup.get('name').enable({emitEvent: false});
} }
this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false});
if (this.displayAdvanced) { this.updateComparisonValidators();
this.dataKeySettingsData.model = this.modelValue.settings; }
this.dataKeySettingsFormGroup.patchValue({
settings: this.dataKeySettingsData private updateComparisonValues() {
}, {emitEvent: false}); 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() { private updateModel() {

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

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

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

@ -65,4 +65,11 @@
border-top: 0; 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 { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; 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 { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeysCallbacks } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields } from '@shared/models/alarm.models';
@ -62,6 +62,8 @@ import {
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { AggregationType } from '@shared/models/time/time.models';
@Component({ @Component({
selector: 'tb-data-keys', selector: 'tb-data-keys',
@ -173,6 +175,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private dialogs: DialogService, private dialogs: DialogService,
private dialog: MatDialog, private dialog: MatDialog,
private fb: FormBuilder, private fb: FormBuilder,
private sanitizer: DomSanitizer,
public truncate: TruncatePipe) { public truncate: TruncatePipe) {
} }
@ -424,6 +427,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
dashboard: this.dashboard, dashboard: this.dashboard,
aliasController: this.aliasController, aliasController: this.aliasController,
widget: this.widget, widget: this.widget,
widgetType: this.widgetType,
entityAliasId: this.entityAliasId, entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm, showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks callbacks: this.callbacks
@ -446,6 +450,36 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
return key ? key.name : undefined; 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>> { private fetchKeys(searchText?: string): Observable<Array<DataKey>> {
if (this.searchText !== searchText || this.latestSearchTextResult === null) { if (this.searchText !== searchText || this.latestSearchTextResult === null) {
this.searchText = searchText; 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 { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import { AggregationType } from '@shared/models/time/time.models';
interface EntitiesTableWidgetSettings extends TableWidgetSettings { interface EntitiesTableWidgetSettings extends TableWidgetSettings {
entitiesTitle: string; entitiesTitle: string;
@ -428,7 +429,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label);
dataKey.title = dataKey.label; dataKey.title = dataKey.label;
dataKey.def = 'def' + this.columns.length; 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; const keySettings: TableWidgetDataKeySettings = dataKey.settings;
if (dataKey.type === DataKeyType.entityField && if (dataKey.type === DataKeyType.entityField &&
!isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') { !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 // tslint:disable-next-line:no-reference
/// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" /> /// <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 { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { ComparisonDuration } from '@shared/models/time/time.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 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; 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 { WidgetContext } from '@home/models/widget-component.models';
import { import {
createLabelFromDatasource, createLabelFromDatasource,
deepClone, deepClone, formattedDataFormDatasourceData,
insertVariable, insertVariable,
isDefined, isDefined,
isDefinedAndNotNull, isDefinedAndNotNull,
@ -28,7 +28,14 @@ import {
isUndefined isUndefined
} from '@app/core/utils'; } from '@app/core/utils';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; 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 { import {
ChartType, ChartType,
TbFlotAxisOptions, TbFlotAxisOptions,
@ -88,6 +95,8 @@ export class TbFlot {
private latestDataThresholds: TbFlotThresholdMarking[]; private latestDataThresholds: TbFlotThresholdMarking[];
private attributesThresholds: TbFlotThresholdMarking[]; private attributesThresholds: TbFlotThresholdMarking[];
private latestData: FormattedData[];
private labelPatternsSourcesSubscription: IWidgetSubscription; private labelPatternsSourcesSubscription: IWidgetSubscription;
private labelPatternsSourcesData: DatasourceData[]; private labelPatternsSourcesData: DatasourceData[];
@ -379,7 +388,7 @@ export class TbFlot {
let tooltipValueFormatFunction: TooltipValueFormatFunction = null; let tooltipValueFormatFunction: TooltipValueFormatFunction = null;
if (this.settings.tooltipValueFormatter && this.settings.tooltipValueFormatter.length) { if (this.settings.tooltipValueFormatter && this.settings.tooltipValueFormatter.length) {
try { try {
tooltipValueFormatFunction = new Function('value', this.settings.tooltipValueFormatter) as TooltipValueFormatFunction; tooltipValueFormatFunction = new Function('value', 'latestData', this.settings.tooltipValueFormatter) as TooltipValueFormatFunction;
} catch (e) { } catch (e) {
tooltipValueFormatFunction = null; tooltipValueFormatFunction = null;
} }
@ -392,7 +401,7 @@ export class TbFlot {
series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction; series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) { if (keySettings.tooltipValueFormatter && keySettings.tooltipValueFormatter.length) {
try { try {
series.dataKey.tooltipValueFormatFunction = new Function('value', series.dataKey.tooltipValueFormatFunction = new Function('value', 'latestData',
keySettings.tooltipValueFormatter) as TooltipValueFormatFunction; keySettings.tooltipValueFormatter) as TooltipValueFormatFunction;
} catch (e) { } catch (e) {
series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction; series.dataKey.tooltipValueFormatFunction = tooltipValueFormatFunction;
@ -546,6 +555,13 @@ export class TbFlot {
} }
this.latestDataThresholds = this.thresholdsSourcesDataUpdated(allThresholds, this.subscription.latestData, true); this.latestDataThresholds = this.thresholdsSourcesDataUpdated(allThresholds, this.subscription.latestData, true);
this.options.grid.markings = allThresholds.concat(this.latestDataThresholds); 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(); this.checkMouseEvents();
@ -653,6 +669,7 @@ export class TbFlot {
this.updateData(); this.updateData();
} }
} else if (this.chartType === 'pie') { } else if (this.chartType === 'pie') {
this.latestData = formattedDataFormDatasourceData(this.subscription.data);
if (this.animatedPie) { if (this.animatedPie) {
this.nextPieDataAnimation(true); this.nextPieDataAnimation(true);
} else { } else {
@ -683,6 +700,11 @@ export class TbFlot {
this.plot.getOptions().grid.markings = this.options.grid.markings; this.plot.getOptions().grid.markings = this.options.grid.markings;
this.updateData(); this.updateData();
} }
if (this.subscription.latestData) {
this.latestData = formattedDataFormDatasourceData(this.subscription.latestData);
} else {
this.latestData = [];
}
} }
} else if (this.isMouseInteraction && this.plot) { } else if (this.isMouseInteraction && this.plot) {
this.latestUpdateTimeoutHandle = setTimeout(this.latestDataUpdate.bind(this), 30); 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() { private scalingPieRadius() {
let scalingLine; let scalingLine;
this.ctx.width > this.ctx.height ? scalingLine = this.ctx.height : scalingLine = this.ctx.width; 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, private seriesInfoDiv(label: string, color: string, value: any,
units: string, trackDecimals: number, active: boolean, units: string, trackDecimals: number, active: boolean,
percent: number, valueFormatFunction: TooltipValueFormatFunction): JQuery<HTMLElement> { percent: number, seriesIndex: number,
valueFormatFunction: TooltipValueFormatFunction): JQuery<HTMLElement> {
const divElement = $('<div></div>'); const divElement = $('<div></div>');
divElement.css({ divElement.css({
display: 'flex', display: 'flex',
@ -1034,7 +1065,7 @@ export class TbFlot {
divElement.append(labelSpan); divElement.append(labelSpan);
let valueContent: string; let valueContent: string;
if (valueFormatFunction) { if (valueFormatFunction) {
valueContent = valueFormatFunction(value); valueContent = valueFormatFunction(value, this.latestDataByDataIndex(seriesIndex));
} else { } else {
valueContent = this.ctx.utils.formatValue(value, trackDecimals, units); 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 units = seriesHoverInfo.units && seriesHoverInfo.units.length ? seriesHoverInfo.units : this.trackUnits;
const decimals = isDefinedAndNotNull(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : this.trackDecimals; const decimals = isDefinedAndNotNull(seriesHoverInfo.decimals) ? seriesHoverInfo.decimals : this.trackDecimals;
const divElement = this.seriesInfoDiv(seriesHoverInfo.label, seriesHoverInfo.color, 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'); 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 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 decimals = isDefinedAndNotNull(item.series.dataKey.decimals) ? item.series.dataKey.decimals : this.trackDecimals;
const divElement = this.seriesInfoDiv(item.series.dataKey.label, item.series.dataKey.color, 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'); 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 <tb-js-func
formControlName="tooltipValueFormatter" formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value']" [functionArgs]="['value', 'latestData']"
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn"> helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func> </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 <tb-js-func
formControlName="tooltipValueFormatter" formControlName="tooltipValueFormatter"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[functionArgs]="['value']" [functionArgs]="['value', 'latestData']"
functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}" functionTitle="{{ 'widgets.chart.tooltip-value-format-function' | translate }}"
helpId="widget/lib/flot/tooltip_value_format_fn"> helpId="widget/lib/flot/tooltip_value_format_fn">
</tb-js-func> </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-group class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static"> <mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px"> <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"> fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs> <div fxLayout="column" fxLayoutGap="8px" fxFlex.gt-xs>
<mat-checkbox formControlName="useDashboardTimewindow"> <mat-checkbox formControlName="useDashboardTimewindow">
@ -34,6 +34,9 @@
style="padding-right: 8px;">widget-config.timewindow</span> style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow asButton="true" <tb-timewindow asButton="true"
isEdit="true" isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow()"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}" aggregation="{{ widgetType === widgetTypes.timeseries }}"
fxFlex formControlName="timewindow"></tb-timewindow> fxFlex formControlName="timewindow"></tb-timewindow>
</section> </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 { AppState } from '@core/core.state';
import { import {
DataKey, DataKey,
Datasource, Datasource, datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation,
DatasourceType, DatasourceType,
datasourceTypeTranslationMap, datasourceTypeTranslationMap,
defaultLegendConfig, defaultLegendConfig,
@ -42,7 +42,7 @@ import {
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; 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 { import {
alarmFields, alarmFields,
AlarmSearchStatus, AlarmSearchStatus,
@ -51,7 +51,7 @@ import {
alarmSeverityTranslations alarmSeverityTranslations
} from '@shared/models/alarm.models'; } from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.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 { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -67,13 +67,14 @@ import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { WidgetActionsData } from './action/manage-widget-actions.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 { 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 { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { AggregationType } from '@shared/models/time/time.models';
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -334,10 +335,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.alarmSourceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({}); this.advancedSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true));
this.dataSettings.addControl('displayTimewindow', this.fb.control(null)); this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true}));
this.dataSettings.addControl('timewindow', this.fb.control(null)); this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true}));
this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => { this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => {
if (value) { if (value) {
this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); this.dataSettings.get('displayTimewindow').disable({emitEvent: false});
@ -467,7 +468,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}, },
{emitEvent: false} {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) ? const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true; config.useDashboardTimewindow : true;
this.dataSettings.patchValue( this.dataSettings.patchValue(
@ -733,6 +734,24 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
!!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); !!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[]>) { public onDatasourceDrop(event: CdkDragDrop<string[]>) {
const datasourcesFormArray = this.datasourcesFormArray(); const datasourcesFormArray = this.datasourcesFormArray();
const datasourceForm = datasourcesFormArray.at(event.previousIndex); const datasourceForm = datasourcesFormArray.at(event.previousIndex);

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

@ -46,6 +46,9 @@
</span> </span>
<tb-timewindow *ngIf="widget.hasTimewindow" <tb-timewindow *ngIf="widget.hasTimewindow"
aggregation="{{widget.hasAggregation}}" aggregation="{{widget.hasAggregation}}"
quickIntervalOnly="{{widget.onlyQuickInterval}}"
historyOnly="{{widget.onlyHistoryTimewindow}}"
alwaysDisplayTypePrefix
timezone="true" timezone="true"
[isEdit]="isEdit" [isEdit]="isEdit"
[(ngModel)]="widgetComponent.widget.config.timewindow" [(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 { 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 { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.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 { Observable, of, Subject } from 'rxjs';
import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils'; import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core'; import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models'; import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable'; import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
export interface WidgetsData { export interface WidgetsData {
widgets: Array<Widget>; widgets: Array<Widget>;
@ -325,6 +333,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
hasAggregation: boolean; hasAggregation: boolean;
onlyQuickInterval: boolean;
onlyHistoryTimewindow: boolean;
style: {[klass: string]: any}; style: {[klass: string]: any};
showWidgetTitlePanel: boolean; 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.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;
this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : 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) ? (isDefined(this.widget.config.useDashboardTimewindow) ?
(!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow) (!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow)
|| this.widget.config.displayTimewindow)) : false) || this.widget.config.displayTimewindow)) : false)
: false; : false;
this.onlyQuickInterval = onlyQuickInterval;
this.onlyHistoryTimewindow = onlyHistoryTimewindow;
this.hasAggregation = this.widget.type === widgetType.timeseries; this.hasAggregation = this.widget.type === widgetType.timeseries;
this.style = { 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({ @Component({
selector: 'tb-quick-time-interval', selector: 'tb-quick-time-interval',
templateUrl: './quick-time-interval.component.html', templateUrl: './quick-time-interval.component.html',
styleUrls: ['./quick-time-interval.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

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

@ -29,32 +29,57 @@
</section> </section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval"> <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideInterval">
<div formGroupName="realtime" class="mat-content mat-padding" style="padding-top: 8px;"> <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"> <mat-radio-button [value]="realtimeTypes.LAST_INTERVAL" color="primary">
<section fxLayout="column"> <section fxLayout="row">
<span translate>timewindow.last</span> <section *ngIf="isEdit" fxLayout="column" style="padding-right: 8px;">
<tb-timeinterval <label class="tb-small hide-label" translate>timewindow.hide</label>
formControlName="timewindowMs" <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideLastInterval"
predefinedName="timewindow.last" (ngModelChange)="onHideLastIntervalChanged()"></mat-checkbox>
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL" </section>
[required]="timewindow.selectedTab === timewindowTypes.REALTIME && <section fxLayout="column">
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.LAST_INTERVAL" <span translate>timewindow.last</span>
style="padding-top: 8px;"></tb-timeinterval> <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> </section>
</mat-radio-button> </mat-radio-button>
<mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary"> <mat-radio-button [value]="realtimeTypes.INTERVAL" color="primary">
<section fxLayout="column"> <section fxLayout="row">
<span translate>timewindow.interval</span> <section *ngIf="isEdit" fxLayout="column" style="padding-right: 8px;">
<tb-quick-time-interval <label class="tb-small hide-label" translate>timewindow.hide</label>
formControlName="quickInterval" <mat-checkbox [ngModelOptions]="{standalone: true}" [(ngModel)]="timewindow.hideQuickInterval"
onlyCurrentInterval="true" (ngModelChange)="onHideQuickIntervalChanged()"></mat-checkbox>
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" </section>
[required]="timewindow.selectedTab === timewindowTypes.REALTIME && <section fxLayout="column">
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" <span translate>timewindow.interval</span>
style="padding-top: 8px"></tb-quick-time-interval> <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> </section>
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </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> </div>
</section> </section>
</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 { export interface TimewindowPanelData {
historyOnly: boolean; historyOnly: boolean;
quickIntervalOnly: boolean;
timewindow: Timewindow; timewindow: Timewindow;
aggregation: boolean; aggregation: boolean;
timezone: boolean; timezone: boolean;
@ -51,6 +52,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
historyOnly = false; historyOnly = false;
quickIntervalOnly = false;
aggregation = false; aggregation = false;
timezone = false; timezone = false;
@ -83,6 +86,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
public viewContainerRef: ViewContainerRef) { public viewContainerRef: ViewContainerRef) {
super(store); super(store);
this.historyOnly = data.historyOnly; this.historyOnly = data.historyOnly;
this.quickIntervalOnly = data.quickIntervalOnly;
this.timewindow = data.timewindow; this.timewindow = data.timewindow;
this.aggregation = data.aggregation; this.aggregation = data.aggregation;
this.timezone = data.timezone; this.timezone = data.timezone;
@ -91,6 +95,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
const hideInterval = this.timewindow.hideInterval || false; 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 hideAggregation = this.timewindow.hideAggregation || false;
const hideAggInterval = this.timewindow.hideAggInterval || false; const hideAggInterval = this.timewindow.hideAggInterval || false;
const hideTimezone = this.timewindow.hideTimezone || 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, ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
disabled: hideInterval disabled: hideInterval
}), }),
timewindowMs: [ timewindowMs: this.fb.control({
this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' value: this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined'
? this.timewindow.realtime.timewindowMs : null ? this.timewindow.realtime.timewindowMs : null,
], disabled: hideInterval || hideLastInterval
}),
interval: [ interval: [
this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined'
? this.timewindow.realtime.interval : null ? this.timewindow.realtime.interval : null
@ -114,7 +121,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit {
quickInterval: this.fb.control({ quickInterval: this.fb.control({
value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined' value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined'
? this.timewindow.realtime.quickInterval : null, ? 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.fixedTimewindow').enable({emitEvent: false});
this.timewindowForm.get('history.quickInterval').enable({emitEvent: false}); this.timewindowForm.get('history.quickInterval').enable({emitEvent: false});
this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false}); this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false});
this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); if (!this.timewindow.hideLastInterval) {
this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); 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(); 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() @Input()
set historyOnly(val) { 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() { get historyOnly() {
return this.historyOnlyValue; 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; aggregationValue = false;
@Input() @Input()
@ -240,6 +268,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
{ {
timewindow: deepClone(this.innerValue), timewindow: deepClone(this.innerValue),
historyOnly: this.historyOnly, historyOnly: this.historyOnly,
quickIntervalOnly: this.quickIntervalOnly,
aggregation: this.aggregation, aggregation: this.aggregation,
timezone: this.timezone, timezone: this.timezone,
isEdit: this.isEdit isEdit: this.isEdit
@ -265,6 +294,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
return Injector.create({parent: this.viewContainerRef.injector, providers}); 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 { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -278,9 +318,15 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
} }
writeValue(obj: Timewindow): void { writeValue(obj: Timewindow): void {
this.innerValue = initModelFromDefaultTimewindow(obj, this.timeService); this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.timeService);
this.timewindowDisabled = this.isTimewindowDisabled(); this.timewindowDisabled = this.isTimewindowDisabled();
this.updateDisplayValue(); if (this.onHistoryOnlyChanged()) {
setTimeout(() => {
this.notifyChanged();
});
} else {
this.updateDisplayValue();
}
} }
notifyChanged() { notifyChanged() {
@ -297,7 +343,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs);
} }
} else { } 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) { if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) {
this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' +
this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); 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 singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1);
export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1024);
export interface EntityCountQuery { export interface EntityCountQuery {
entityFilter: EntityFilter; entityFilter: EntityFilter;
@ -761,12 +760,19 @@ export interface AlarmDataQuery extends AbstractDataQuery<AlarmDataPageLink> {
export interface TsValue { export interface TsValue {
ts: number; ts: number;
value: string; value: string;
count?: number;
}
export interface ComparisonTsValue {
current?: TsValue;
previous?: TsValue;
} }
export interface EntityData { export interface EntityData {
entityId: EntityId; entityId: EntityId;
latest: {[entityKeyType: string]: {[key: string]: TsValue}}; latest: {[entityKeyType: string]: {[key: string]: TsValue}};
timeseries: {[key: string]: Array<TsValue>}; timeseries: {[key: string]: Array<TsValue>};
aggLatest?: {[id: number]: ComparisonTsValue};
} }
export interface AlarmData extends AlarmInfo { 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; 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 { export class EntityDataCmd implements WebsocketCmd {
cmdId: number; cmdId: number;
query?: EntityDataQuery; query?: EntityDataQuery;
historyCmd?: EntityHistoryCmd; historyCmd?: EntityHistoryCmd;
latestCmd?: LatestValueCmd; latestCmd?: LatestValueCmd;
tsCmd?: TimeSeriesCmd; tsCmd?: TimeSeriesCmd;
aggHistoryCmd?: AggEntityHistoryCmd;
aggTsCmd?: AggTimeSeriesCmd;
public isEmpty(): boolean { public isEmpty(): boolean {
return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd; return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd;
@ -212,15 +235,6 @@ export class AlarmDataUnsubscribeCmd implements WebsocketCmd {
} }
export class TelemetryPluginCmdsWrapper { 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() { constructor() {
this.attrSubCmds = []; this.attrSubCmds = [];
@ -233,6 +247,24 @@ export class TelemetryPluginCmdsWrapper {
this.entityCountCmds = []; this.entityCountCmds = [];
this.entityCountUnsubscribeCmds = []; 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 { public hasCommands(): boolean {
return this.tsSubCmds.length > 0 || return this.tsSubCmds.length > 0 ||
@ -261,38 +293,33 @@ export class TelemetryPluginCmdsWrapper {
public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper {
const preparedWrapper = new TelemetryPluginCmdsWrapper(); const preparedWrapper = new TelemetryPluginCmdsWrapper();
let leftCount = maxCommands; let leftCount = maxCommands;
preparedWrapper.tsSubCmds = this.popCmds(this.tsSubCmds, leftCount); preparedWrapper.tsSubCmds = TelemetryPluginCmdsWrapper.popCmds(this.tsSubCmds, leftCount);
leftCount -= preparedWrapper.tsSubCmds.length; leftCount -= preparedWrapper.tsSubCmds.length;
preparedWrapper.historyCmds = this.popCmds(this.historyCmds, leftCount); preparedWrapper.historyCmds = TelemetryPluginCmdsWrapper.popCmds(this.historyCmds, leftCount);
leftCount -= preparedWrapper.historyCmds.length; leftCount -= preparedWrapper.historyCmds.length;
preparedWrapper.attrSubCmds = this.popCmds(this.attrSubCmds, leftCount); preparedWrapper.attrSubCmds = TelemetryPluginCmdsWrapper.popCmds(this.attrSubCmds, leftCount);
leftCount -= preparedWrapper.attrSubCmds.length; leftCount -= preparedWrapper.attrSubCmds.length;
preparedWrapper.entityDataCmds = this.popCmds(this.entityDataCmds, leftCount); preparedWrapper.entityDataCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityDataCmds, leftCount);
leftCount -= preparedWrapper.entityDataCmds.length; leftCount -= preparedWrapper.entityDataCmds.length;
preparedWrapper.entityDataUnsubscribeCmds = this.popCmds(this.entityDataUnsubscribeCmds, leftCount); preparedWrapper.entityDataUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityDataUnsubscribeCmds, leftCount);
leftCount -= preparedWrapper.entityDataUnsubscribeCmds.length; leftCount -= preparedWrapper.entityDataUnsubscribeCmds.length;
preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); preparedWrapper.alarmDataCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmDataCmds, leftCount);
leftCount -= preparedWrapper.alarmDataCmds.length; leftCount -= preparedWrapper.alarmDataCmds.length;
preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount); preparedWrapper.alarmDataUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmDataUnsubscribeCmds, leftCount);
leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length; leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length;
preparedWrapper.entityCountCmds = this.popCmds(this.entityCountCmds, leftCount); preparedWrapper.entityCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountCmds, leftCount);
leftCount -= preparedWrapper.entityCountCmds.length; leftCount -= preparedWrapper.entityCountCmds.length;
preparedWrapper.entityCountUnsubscribeCmds = this.popCmds(this.entityCountUnsubscribeCmds, leftCount); preparedWrapper.entityCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountUnsubscribeCmds, leftCount);
return preparedWrapper; 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 { export interface SubscriptionData {
[key: string]: [number, any][]; [key: string]: [number, any, number?][];
}
export interface IndexedSubscriptionData {
[id: number]: [number, any, number?][];
} }
export interface SubscriptionDataHolder { export interface SubscriptionDataHolder {
@ -434,16 +461,7 @@ export class EntityDataUpdate extends DataUpdate<EntityData> {
super(msg); super(msg);
} }
public prepareData(tsOffset: number) { private static processEntityData(data: Array<EntityData>, 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) {
for (const entityData of data) { for (const entityData of data) {
if (entityData.timeseries) { if (entityData.timeseries) {
for (const key of Object.keys(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> { export class AlarmDataUpdate extends DataUpdate<AlarmData> {
allowedEntities: number;
totalEntities: number;
constructor(msg: AlarmDataUpdateMsg) { constructor(msg: AlarmDataUpdateMsg) {
super(msg); super(msg);
this.allowedEntities = msg.allowedEntities; this.allowedEntities = msg.allowedEntities;
this.totalEntities = msg.totalEntities; this.totalEntities = msg.totalEntities;
} }
allowedEntities: number;
totalEntities: number;
public prepareData(tsOffset: number) { private static processAlarmData(data: Array<AlarmData>, 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) {
for (const alarmData of data) { for (const alarmData of data) {
alarmData.createdTime += tsOffset; alarmData.createdTime += tsOffset;
if (alarmData.ackTs) { 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 { 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; displayValue?: string;
displayTimezoneAbbr?: string; displayTimezoneAbbr?: string;
hideInterval?: boolean; hideInterval?: boolean;
hideQuickInterval?: boolean;
hideLastInterval?: boolean;
hideAggregation?: boolean; hideAggregation?: boolean;
hideAggInterval?: boolean; hideAggInterval?: boolean;
hideTimezone?: boolean; hideTimezone?: boolean;
@ -188,6 +190,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow {
return { return {
displayValue: '', displayValue: '',
hideInterval: false, hideInterval: false,
hideLastInterval: false,
hideQuickInterval: false,
hideAggregation: false, hideAggregation: false,
hideAggInterval: false, hideAggInterval: false,
hideTimezone: 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); const model = defaultTimewindow(timeService);
if (value) { if (value) {
model.hideInterval = value.hideInterval; model.hideInterval = value.hideInterval;
model.hideLastInterval = value.hideLastInterval;
model.hideQuickInterval = value.hideQuickInterval;
model.hideAggregation = value.hideAggregation; model.hideAggregation = value.hideAggregation;
model.hideAggInterval = value.hideAggInterval; model.hideAggInterval = value.hideAggInterval;
model.hideTimezone = value.hideTimezone; model.hideTimezone = value.hideTimezone;
@ -281,6 +287,9 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T
} }
model.timezone = value.timezone; model.timezone = value.timezone;
} }
if (quickIntervalOnly) {
model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
}
return model; return model;
} }
@ -304,6 +313,8 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number,
} }
return { return {
hideInterval: timewindow.hideInterval || false, hideInterval: timewindow.hideInterval || false,
hideLastInterval: timewindow.hideLastInterval || false,
hideQuickInterval: timewindow.hideQuickInterval || false,
hideAggregation: timewindow.hideAggregation || false, hideAggregation: timewindow.hideAggregation || false,
hideAggInterval: timewindow.hideAggInterval || false, hideAggInterval: timewindow.hideAggInterval || false,
hideTimezone: timewindow.hideTimezone || false, hideTimezone: timewindow.hideTimezone || false,
@ -694,6 +705,8 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti
export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow {
const cloned: Timewindow = {}; const cloned: Timewindow = {};
cloned.hideInterval = timewindow.hideInterval || false; cloned.hideInterval = timewindow.hideInterval || false;
cloned.hideLastInterval = timewindow.hideLastInterval || false;
cloned.hideQuickInterval = timewindow.hideQuickInterval || false;
cloned.hideAggregation = timewindow.hideAggregation || false; cloned.hideAggregation = timewindow.hideAggregation || false;
cloned.hideAggInterval = timewindow.hideAggInterval || false; cloned.hideAggInterval = timewindow.hideAggInterval || false;
cloned.hideTimezone = timewindow.hideTimezone || 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 { BaseData } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-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 { EntityType } from '@shared/models/entity-type.models';
import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models'; import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models';
import { DataKeyType } from './telemetry/telemetry.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 { export interface KeyInfo {
name: string; name: string;
aggregationType?: AggregationType;
comparisonEnabled?: boolean;
timeForComparison?: ComparisonDuration;
comparisonCustomIntervalValue?: number;
comparisonResultType?: ComparisonResultType;
label?: string; label?: string;
color?: string; color?: string;
funcBody?: string; funcBody?: string;
@ -269,6 +288,18 @@ export interface KeyInfo {
decimals?: number; 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 { export interface DataKey extends KeyInfo {
type: DataKeyType; type: DataKeyType;
pattern?: string; pattern?: string;
@ -322,6 +353,37 @@ export interface Datasource {
[key: string]: any; [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 { export interface FormattedData {
$datasource: Datasource; $datasource: Datasource;
entityName: string; entityName: string;

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

@ -1039,7 +1039,22 @@
"value-description": "the current value;", "value-description": "the current value;",
"prev-value-description": "result of the previous function call;", "prev-value-description": "result of the previous function call;",
"time-prev-description": "timestamp of the previous value;", "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": { "datasource": {
"type": "Datasource type", "type": "Datasource type",

Loading…
Cancel
Save