Browse Source

Merge remote-tracking branch 'origin/master' into tbel_new_ver_Unmodif_FunctionDate

pull/12804/head
nick 2 years ago
parent
commit
1e46b6c62b
  1. 6
      .github/workflows/check-configuration-files.yml
  2. 19
      application/src/main/data/upgrade/basic/schema_update.sql
  3. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 34
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  5. 15
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java
  8. 36
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  9. 119
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java
  10. 2
      application/src/main/resources/thingsboard.yml
  11. 85
      application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java
  12. 24
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  13. 2
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
  14. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  15. 1
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java
  16. 9
      common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java
  17. 10
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  18. 3
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  19. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java
  20. 7
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  21. 66
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java
  22. 103
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java
  23. 61
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java
  24. 10
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  25. 88
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java
  26. 2
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  27. 7
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  28. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  29. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java
  30. 39
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java
  31. 51
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  32. 5
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  33. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  34. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html
  35. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts
  36. 8
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  37. 9
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  38. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  39. 89
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  40. 10
      ui-ngx/src/app/shared/models/tenant.model.ts

6
.github/workflows/check-configuration-files.yml

@ -32,14 +32,14 @@ on:
jobs: jobs:
build: build:
name: Check thingsboard.yml file name: Check thingsboard.yml file
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python 3.10 - name: Set up Python 3.13
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: "3.10.2" python-version: "3.13.2"
architecture: "x64" architecture: "x64"
env: env:
AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache

19
application/src/main/data/upgrade/basic/schema_update.sql

@ -63,4 +63,21 @@ $$;
-- UPDATE SAVE TIME SERIES NODES END -- UPDATE SAVE TIME SERIES NODES END
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS START
UPDATE tenant_profile
SET profile_data = profile_data
|| jsonb_build_object(
'configuration', profile_data->'configuration' || jsonb_build_object(
'maxCalculatedFieldsPerEntity', COALESCE(profile_data->'configuration'->>'maxCalculatedFieldsPerEntity', '5')::bigint,
'maxArgumentsPerCF', COALESCE(profile_data->'configuration'->>'maxArgumentsPerCF', '10')::bigint,
'maxDataPointsPerRollingArg', COALESCE(profile_data->'configuration'->>'maxDataPointsPerRollingArg', '1000')::bigint,
'maxStateSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxStateSizeInKBytes', '32')::bigint,
'maxSingleValueArgumentSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxSingleValueArgumentSizeInKBytes', '2')::bigint
)
)
WHERE profile_data->'configuration'->>'maxCalculatedFieldsPerEntity' IS NULL;
-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS END

4
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -644,6 +644,10 @@ public class ActorSystemContext {
@Getter @Getter
private String deviceStateNodeRateLimitConfig; private String deviceStateNodeRateLimitConfig;
@Value("${actors.calculated_fields.calculation_timeout:5}")
@Getter
private long cfCalculationResultTimeout;
@Getter @Getter
@Setter @Setter
private TbActorSystem actorSystem; private TbActorSystem actorSystem;

34
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -274,32 +274,30 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException {
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
boolean stateSizeOk; boolean stateSizeChecked = false;
if (ctx.isInitialized() && state.isReady()) { try {
try { if (ctx.isInitialized() && state.isReady()) {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS); CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize()); state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeOk = state.isSizeOk(); stateSizeChecked = true;
if (stateSizeOk) { if (state.isSizeOk()) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null); systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null);
} }
} }
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
} }
} else { } catch (Exception e) {
state.checkStateSize(ctxId, ctx.getMaxStateSize()); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
stateSizeOk = state.isSizeOk(); } finally {
if (stateSizeOk) { if (!stateSizeChecked) {
callback.onSuccess(); // State was updated but no calculation performed; state.checkStateSize(ctxId, ctx.getMaxStateSize());
}
if (state.isSizeOk()) {
cfStateService.persistState(ctxId, state, callback);
} else {
removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback);
} }
}
if (stateSizeOk) {
cfStateService.persistState(ctxId, state, callback);
} else {
removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback);
} }
} }

15
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
@ -64,7 +65,19 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
double expressionResult = expr.evaluate(); double expressionResult = expr.evaluate();
Output output = ctx.getOutput(); Output output = ctx.getOutput();
return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), JacksonUtil.valueToTree(Map.of(output.getName(), expressionResult)))); Object result;
Integer decimals = output.getDecimalsByDefault();
if (decimals != null) {
if (decimals.equals(0)) {
result = TbUtils.toInt(expressionResult);
} else {
result = TbUtils.toFixed(expressionResult, decimals);
}
} else {
result = expressionResult;
}
return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), JacksonUtil.valueToTree(Map.of(output.getName(), result))));
} }
} }

4
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -100,10 +100,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
if (newVersion == null || this.version == null || newVersion > this.version) { if (newVersion == null || this.version == null || newVersion > this.version) {
this.ts = singleValueEntry.getTs(); this.ts = singleValueEntry.getTs();
this.version = newVersion; this.version = newVersion;
BasicKvEntry newValue = singleValueEntry.getKvEntryValue();
if (this.kvEntryValue != null && this.kvEntryValue.getValue().equals(newValue.getValue())) {
return false;
}
this.kvEntryValue = singleValueEntry.getKvEntryValue(); this.kvEntryValue = singleValueEntry.getKvEntryValue();
return true; return true;
} }

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java

@ -89,7 +89,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
for (var e : tsRecords.entrySet()) { for (var e : tsRecords.entrySet()) {
values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue())); values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
} }
return new TbelCfTsRollingArg(limit, timeWindow, values); return new TbelCfTsRollingArg(timeWindow, values);
} }
@Override @Override

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

@ -33,7 +33,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId;
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.ReadTsKvQueryResult;
@ -41,7 +41,6 @@ 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.ComparisonTsValue;
import org.thingsboard.server.common.data.query.OriginatorAlarmFilter;
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;
@ -55,17 +54,16 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; 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.security.model.SecurityUser;
import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggHistoryCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AggTimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.CmdUpdate;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate;
@ -74,7 +72,6 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.GetTsCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.UnsubscribeCmd;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -83,7 +80,6 @@ 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.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -430,13 +426,25 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
ctx.fetchData(); ctx.fetchData();
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
stats.getAlarmQueryInvocationCnt().incrementAndGet(); stats.getRegularQueryInvocationCnt().incrementAndGet();
stats.getAlarmQueryTimeSpent().addAndGet(end - start); stats.getRegularQueryTimeSpent().addAndGet(end - start);
TbAlarmCountSubCtx finalCtx = ctx; Set<EntityId> entitiesIds = ctx.getEntitiesIds();
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay( ctx.cancelTasks();
() -> refreshDynamicQuery(finalCtx), ctx.clearAlarmSubscriptions();
dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); if (entitiesIds != null && entitiesIds.isEmpty()) {
finalCtx.setRefreshTask(task); AlarmCountUpdate update = new AlarmCountUpdate(cmd.getCmdId(), 0);
ctx.sendWsMsg(update);
} else {
ctx.doFetchAlarmCount();
if (entitiesIds != null) {
ctx.createAlarmSubscriptions();
}
TbAlarmCountSubCtx finalCtx = ctx;
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(
() -> refreshDynamicQuery(finalCtx),
dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
finalCtx.setRefreshTask(task);
}
} else { } else {
log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd); log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd);
} }
@ -555,7 +563,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) { private TbAlarmCountSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmCountCmd cmd) {
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>()); Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService, TbAlarmCountSubCtx ctx = new TbAlarmCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, alarmService, sessionRef, cmd.getCmdId()); attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription, maxAlarmQueriesPerRefreshInterval);
if (cmd.getQuery() != null) { if (cmd.getQuery() != null) {
ctx.setAndResolveQuery(cmd.getQuery()); ctx.setAndResolveQuery(cmd.getQuery());
} }

119
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmCountSubCtx.java

@ -19,49 +19,152 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.service.ws.WebSocketService; import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@ToString(callSuper = true) @ToString(callSuper = true)
public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx<AlarmCountQuery> { public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx<AlarmCountQuery> {
private final AlarmService alarmService; private final AlarmService alarmService;
protected final Map<Integer, EntityId> subToEntityIdMap;
@Getter
private LinkedHashSet<EntityId> entitiesIds;
private final int maxEntitiesPerAlarmSubscription;
private final int maxAlarmQueriesPerRefreshInterval;
@Getter @Getter
@Setter @Setter
private volatile int result; private volatile int result;
@Getter
@Setter
private boolean tooManyEntities;
private int alarmCountInvocationAttempts;
public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService, public TbAlarmCountSubCtx(String serviceId, WebSocketService wsService,
EntityService entityService, TbLocalSubscriptionService localSubscriptionService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService, AttributesService attributesService, SubscriptionServiceStatistics stats, AlarmService alarmService,
WebSocketSessionRef sessionRef, int cmdId) { WebSocketSessionRef sessionRef, int cmdId, int maxEntitiesPerAlarmSubscription, int maxAlarmQueriesPerRefreshInterval) {
super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId); super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
this.alarmService = alarmService; this.alarmService = alarmService;
this.subToEntityIdMap = new ConcurrentHashMap<>();
this.maxEntitiesPerAlarmSubscription = maxEntitiesPerAlarmSubscription;
this.maxAlarmQueriesPerRefreshInterval = maxAlarmQueriesPerRefreshInterval;
this.entitiesIds = null;
}
@Override
public void clearSubscriptions() {
clearAlarmSubscriptions();
} }
@Override @Override
public void fetchData() { public void fetchData() {
result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); resetInvocationCounter();
sendWsMsg(new AlarmCountUpdate(cmdId, result)); if (query.getEntityFilter() != null) {
entitiesIds = new LinkedHashSet<>();
log.trace("[{}] Fetching data: {}", cmdId, alarmCountInvocationAttempts);
PageData<EntityData> data = entityService.findEntityDataByQuery(getTenantId(), getCustomerId(), buildEntityDataQuery());
entitiesIds.clear();
tooManyEntities = data.hasNext();
for (EntityData entityData : data.getData()) {
entitiesIds.add(entityData.getEntityId());
}
}
} }
@Override @Override
protected void update() { protected void update() {
int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query); resetInvocationCounter();
if (newCount != result) { fetchAlarmCount();
result = newCount;
sendWsMsg(new AlarmCountUpdate(cmdId, result));
}
} }
@Override @Override
public boolean isDynamic() { public boolean isDynamic() {
return true; return true;
} }
public void fetchAlarmCount() {
alarmCountInvocationAttempts++;
log.trace("[{}] Fetching alarms: {}", cmdId, alarmCountInvocationAttempts);
if (alarmCountInvocationAttempts <= maxAlarmQueriesPerRefreshInterval) {
int newCount = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entitiesIds);
if (newCount != result) {
result = newCount;
sendWsMsg(new AlarmCountUpdate(cmdId, result));
}
} else {
log.trace("[{}] Ignore alarm count fetch due to rate limit: [{}] of maximum [{}]", cmdId, alarmCountInvocationAttempts, maxAlarmQueriesPerRefreshInterval);
}
}
public void doFetchAlarmCount() {
result = (int) alarmService.countAlarmsByQuery(getTenantId(), getCustomerId(), query, entitiesIds);
sendWsMsg(new AlarmCountUpdate(cmdId, result));
}
private EntityDataQuery buildEntityDataQuery() {
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null,
new EntityDataSortOrder(new EntityKey(EntityKeyType.ENTITY_FIELD, ModelConstants.CREATED_TIME_PROPERTY)));
return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters());
}
private void resetInvocationCounter() {
alarmCountInvocationAttempts = 0;
}
public void createAlarmSubscriptions() {
for (EntityId entityId : entitiesIds) {
createAlarmSubscriptionForEntity(entityId);
}
}
private void createAlarmSubscriptionForEntity(EntityId entityId) {
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
subToEntityIdMap.put(subIdx, entityId);
log.trace("[{}][{}][{}] Creating alarms subscription for [{}] ", serviceId, cmdId, subIdx, entityId);
TbAlarmsSubscription subscription = TbAlarmsSubscription.builder()
.serviceId(serviceId)
.sessionId(sessionRef.getSessionId())
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityId)
.updateProcessor((sub, update) -> fetchAlarmCount())
.build();
localSubscriptionService.addSubscription(subscription, sessionRef);
}
public void clearAlarmSubscriptions() {
if (subToEntityIdMap != null) {
for (Integer subId : subToEntityIdMap.keySet()) {
localSubscriptionService.cancelSubscription(getTenantId(), getSessionId(), subId);
}
subToEntityIdMap.clear();
}
}
} }

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

@ -512,6 +512,8 @@ actors:
enabled: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}" enabled: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}"
# The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour # The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour
configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}" configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}"
# Time in seconds to receive calculation result.
calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}"
debug: debug:
settings: settings:

85
application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java

@ -83,7 +83,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Slf4j @Slf4j
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"server.ws.alarms_per_alarm_status_subscription_cache_size=5" "server.ws.alarms_per_alarm_status_subscription_cache_size=5",
"server.ws.dynamic_page_link.refresh_interval=15"
}) })
public class WebsocketApiTest extends AbstractControllerTest { public class WebsocketApiTest extends AbstractControllerTest {
@Autowired @Autowired
@ -324,6 +325,83 @@ public class WebsocketApiTest extends AbstractControllerTest {
Assert.assertEquals(1, update.getCount()); Assert.assertEquals(1, update.getCount());
} }
@Test
public void testAlarmCountWsCmdWithSingleEntityFilter() throws Exception {
loginTenantAdmin();
SingleEntityFilter singleEntityFilter = new SingleEntityFilter();
singleEntityFilter.setSingleEntity(tenantId);
AlarmCountQuery alarmCountQuery = new AlarmCountQuery(singleEntityFilter);
AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery);
getWsClient().send(cmd1);
AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(0, update.getCount());
//create alarm, check count = 1
getWsClient().registerWaitForUpdate();
Alarm alarm = new Alarm();
alarm.setOriginator(tenantId);
alarm.setType("TEST ALARM");
alarm.setSeverity(AlarmSeverity.WARNING);
alarm = doPost("/api/alarm", alarm, Alarm.class);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(1, update.getCount());
// set wrong entity id in filter, check count = 0
singleEntityFilter.setSingleEntity(tenantAdminUserId);
AlarmCountCmd cmd3 = new AlarmCountCmd(2, alarmCountQuery);
getWsClient().send(cmd3);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(2, update.getCmdId());
Assert.assertEquals(0, update.getCount());
}
@Test
public void testAlarmCountWsCmdWithDeviceType() throws Exception {
loginTenantAdmin();
DeviceTypeFilter deviceTypeFilter = new DeviceTypeFilter();
deviceTypeFilter.setDeviceTypes(List.of("default"));
AlarmCountQuery alarmCountQuery = new AlarmCountQuery(deviceTypeFilter);
AlarmCountCmd cmd1 = new AlarmCountCmd(1, alarmCountQuery);
getWsClient().send(cmd1);
AlarmCountUpdate update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(0, update.getCount());
getWsClient().registerWaitForUpdate();
Alarm alarm = new Alarm();
alarm.setOriginator(device.getId());
alarm.setType("TEST ALARM");
alarm.setSeverity(AlarmSeverity.WARNING);
alarm = doPost("/api/alarm", alarm, Alarm.class);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForUpdate());
Assert.assertEquals(1, update.getCmdId());
Assert.assertEquals(1, update.getCount());
deviceTypeFilter.setDeviceTypes(List.of("non-existing"));
AlarmCountCmd cmd3 = new AlarmCountCmd(3, alarmCountQuery);
getWsClient().send(cmd3);
update = getWsClient().parseAlarmCountReply(getWsClient().waitForReply());
Assert.assertEquals(3, update.getCmdId());
Assert.assertEquals(0, update.getCount());
}
@Test @Test
public void testAlarmStatusWsCmd() throws Exception { public void testAlarmStatusWsCmd() throws Exception {
loginTenantAdmin(); loginTenantAdmin();
@ -372,17 +450,18 @@ public class WebsocketApiTest extends AbstractControllerTest {
doPost("/api/alarm", alarm2, Alarm.class); doPost("/api/alarm", alarm2, Alarm.class);
AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); AlarmStatusUpdate alarmStatusUpdate3 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class);
Assert.assertEquals(1, alarmStatusUpdate3.getCmdId()); Assert.assertEquals(1, alarmStatusUpdate3.getCmdId());
Assert.assertTrue(alarmStatusUpdate3.isActive()); Assert.assertTrue(alarmStatusUpdate3.isActive());
//change severity //change severity
getWsClient().registerWaitForUpdate();
alarm2.setSeverity(AlarmSeverity.MAJOR); alarm2.setSeverity(AlarmSeverity.MAJOR);
Alarm updatedAlarm = doPost("/api/alarm", alarm2, Alarm.class); Alarm updatedAlarm = doPost("/api/alarm", alarm2, Alarm.class);
Assert.assertNotNull(updatedAlarm); Assert.assertNotNull(updatedAlarm);
Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity());
AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForReply(), AlarmStatusUpdate.class); AlarmStatusUpdate alarmStatusUpdate4 = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class);
Assert.assertEquals(1, alarmStatusUpdate4.getCmdId()); Assert.assertEquals(1, alarmStatusUpdate4.getCmdId());
Assert.assertFalse(alarmStatusUpdate4.isActive()); Assert.assertFalse(alarmStatusUpdate4.isActive());

24
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedField
import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
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.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
@ -138,7 +139,7 @@ public class SimpleCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput(); Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49.0))); assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49)));
} }
@Test @Test
@ -154,6 +155,26 @@ public class SimpleCalculatedFieldStateTest {
.hasMessage("Argument 'key2' is not a number."); .hasMessage("Argument 'key2' is not a number.");
} }
@Test
void testPerformCalculationWhenDecimalsByDefault() throws ExecutionException, InterruptedException {
state.arguments = new HashMap<>(Map.of(
"key1", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("key1", 11.3456), 145L),
"key2", new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("key2", 15.1), 165L),
"key3", new SingleValueArgumentEntry(System.currentTimeMillis() - 3, new DoubleDataEntry("key3", 23.1), 184L)
));
Output output = getCalculatedFieldConfig().getOutput();
output.setDecimalsByDefault(3);
ctx.setOutput(output);
CalculatedFieldResult result = state.performCalculation(ctx).get();
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49.546)));
}
@Test @Test
void testIsReadyWhenNotAllArgPresent() { void testIsReadyWhenNotAllArgPresent() {
assertThat(state.isReady()).isFalse(); assertThat(state.isReady()).isFalse();
@ -219,6 +240,7 @@ public class SimpleCalculatedFieldStateTest {
output.setName("output"); output.setName("output");
output.setType(OutputType.ATTRIBUTES); output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE); output.setScope(AttributeScope.SERVER_SCOPE);
output.setDecimalsByDefault(0);
config.setOutput(output); config.setOutput(output);

2
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java

@ -71,6 +71,6 @@ public class SingleValueArgumentEntryTest {
@Test @Test
void testUpdateEntryWhenValueWasNotChanged() { void testUpdateEntryWhenValueWasNotChanged() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 237L))).isFalse(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue();
} }
} }

2
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -118,6 +118,8 @@ public interface AlarmService extends EntityDaoService {
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds);
PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink); PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink);
List<UUID> findActiveOriginatorAlarms(TenantId tenantId, OriginatorAlarmFilter originatorAlarmFilter, int limit); List<UUID> findActiveOriginatorAlarms(TenantId tenantId, OriginatorAlarmFilter originatorAlarmFilter, int limit);

1
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java

@ -26,5 +26,6 @@ public class Output {
private String name; private String name;
private OutputType type; private OutputType type;
private AttributeScope scope; private AttributeScope scope;
private Integer decimalsByDefault;
} }

9
common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmCountQuery.java

@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.query;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString; import lombok.ToString;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
@ -29,7 +29,7 @@ import java.util.List;
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Getter @Data
@ToString @ToString
public class AlarmCountQuery extends EntityCountQuery { public class AlarmCountQuery extends EntityCountQuery {
private long startTs; private long startTs;
@ -40,4 +40,9 @@ public class AlarmCountQuery extends EntityCountQuery {
private List<AlarmSeverity> severityList; private List<AlarmSeverity> severityList;
private boolean searchPropagatedAlarms; private boolean searchPropagatedAlarms;
private UserId assigneeId; private UserId assigneeId;
public AlarmCountQuery(EntityFilter entityFilter) {
super(entityFilter);
}
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -135,11 +135,11 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private double warnThreshold; private double warnThreshold;
private long maxCalculatedFieldsPerEntity; private long maxCalculatedFieldsPerEntity = 5;
private long maxArgumentsPerCF; private long maxArgumentsPerCF = 10;
private long maxDataPointsPerRollingArg; private long maxDataPointsPerRollingArg = 1000;
private long maxStateSizeInKBytes; private long maxStateSizeInKBytes = 32;
private long maxSingleValueArgumentSizeInKBytes; private long maxSingleValueArgumentSizeInKBytes = 2;
@Override @Override
public long getProfileThreshold(ApiUsageRecordKey key) { public long getProfileThreshold(ApiUsageRecordKey key) {

3
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java

@ -136,7 +136,10 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize); parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize);
parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize); parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize); parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize);
parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize);
parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize); parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize);
TbUtils.register(parserConfig); TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor")); executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
try { try {

6
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbTimeWindow.java

@ -17,20 +17,24 @@ package org.thingsboard.script.api.tbel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor
public class TbTimeWindow implements TbelCfObject { public class TbTimeWindow implements TbelCfObject {
public static final long OBJ_SIZE = 32L; public static final long OBJ_SIZE = 32L;
private long startTs; private long startTs;
private long endTs; private long endTs;
private int limit;
@Override @Override
public long memorySize() { public long memorySize() {
return OBJ_SIZE; return OBJ_SIZE;
} }
public boolean matches(long ts) {
return ts >= startTs && ts < endTs;
}
} }

7
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -255,6 +255,8 @@ public class TbUtils {
double.class, int.class))); double.class, int.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed", parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
float.class, int.class))); float.class, int.class)));
parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt",
double.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes", parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class))); ExecutionContext.class, String.class)));
parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray", parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray",
@ -1155,6 +1157,10 @@ public class TbUtils {
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue(); return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue();
} }
public static int toInt(double value) {
return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue();
}
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json) { public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json) {
return toFlatMap(ctx, json, new ArrayList<>(), true); return toFlatMap(ctx, json, new ArrayList<>(), true);
} }
@ -1506,5 +1512,6 @@ public class TbUtils {
} }
return hex; return hex;
} }
} }

66
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsMultiDoubleVal.java

@ -0,0 +1,66 @@
/**
* Copyright © 2016-2025 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.script.api.tbel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
@Data
public class TbelCfTsMultiDoubleVal implements TbelCfObject {
public static final long OBJ_SIZE = 32L; // Approximate calculation;
private final long ts;
private final double[] values;
@JsonIgnore
public double getV1() {
return getV(0);
}
@JsonIgnore
public double getV2() {
return getV(1);
}
@JsonIgnore
public double getV3() {
return getV(2);
}
@JsonIgnore
public double getV4() {
return getV(3);
}
@JsonIgnore
public double getV5() {
return getV(4);
}
private double getV(int idx) {
if (values.length < idx + 1) {
throw new IllegalArgumentException("Can't get value at index " + idx + ". There are " + values.length + " values present.");
} else {
return values[idx];
}
}
@Override
public long memorySize() {
return OBJ_SIZE + values.length * 8L;
}
}

103
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java

@ -19,11 +19,15 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; 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.common.util.JacksonUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.function.Consumer; import java.util.function.Consumer;
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE; import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
@ -44,9 +48,9 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
this.values = Collections.unmodifiableList(values); this.values = Collections.unmodifiableList(values);
} }
public TbelCfTsRollingArg(int limit, long timeWindow, List<TbelCfTsDoubleVal> values) { public TbelCfTsRollingArg(long timeWindow, List<TbelCfTsDoubleVal> values) {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
this.timeWindow = new TbTimeWindow(ts - timeWindow, ts, limit); this.timeWindow = new TbTimeWindow(ts - timeWindow, ts);
this.values = Collections.unmodifiableList(values); this.values = Collections.unmodifiableList(values);
} }
@ -104,6 +108,14 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
return min; return min;
} }
public double avg() {
return avg(true);
}
public double avg(boolean ignoreNaN) {
return mean(ignoreNaN);
}
public double mean() { public double mean() {
return mean(true); return mean(true);
} }
@ -256,6 +268,88 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
return sum; return sum;
} }
public TbelCfTsRollingData merge(TbelCfTsRollingArg other) {
return mergeAll(Collections.singletonList(other), null);
}
public TbelCfTsRollingData merge(TbelCfTsRollingArg other, Map<String, Object> settings) {
return mergeAll(Collections.singletonList(other), settings);
}
public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others) {
return mergeAll(others, null);
}
public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others, Map<String, Object> settings) {
List<TbelCfTsRollingArg> args = new ArrayList<>(others.size() + 1);
args.add(this);
args.addAll(others);
boolean ignoreNaN = true;
if (settings != null && settings.containsKey("ignoreNaN")) {
ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString());
}
TbTimeWindow timeWindow = null;
if (settings != null && settings.containsKey("timeWindow")) {
var twVar = settings.get("timeWindow");
if (twVar instanceof TbTimeWindow) {
timeWindow = (TbTimeWindow) settings.get("timeWindow");
} else if (twVar instanceof Map twMap) {
timeWindow = new TbTimeWindow(Long.valueOf(twMap.get("startTs").toString()), Long.valueOf(twMap.get("endTs").toString()));
} else {
timeWindow = JacksonUtil.fromString(settings.get("timeWindow").toString(), TbTimeWindow.class);
}
}
TreeSet<Long> allTimestamps = new TreeSet<>();
long startTs = Long.MAX_VALUE;
long endTs = Long.MIN_VALUE;
for (TbelCfTsRollingArg arg : args) {
for (TbelCfTsDoubleVal val : arg.getValues()) {
allTimestamps.add(val.getTs());
}
startTs = Math.min(startTs, arg.getTimeWindow().getStartTs());
endTs = Math.max(endTs, arg.getTimeWindow().getEndTs());
}
List<TbelCfTsMultiDoubleVal> data = new ArrayList<>();
int[] lastIndex = new int[args.size()];
double[] result = new double[args.size()];
Arrays.fill(result, Double.NaN);
for (long ts : allTimestamps) {
for (int i = 0; i < args.size(); i++) {
var arg = args.get(i);
var values = arg.getValues();
while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) {
result[i] = values.get(lastIndex[i]).getValue();
lastIndex[i]++;
}
}
if (timeWindow == null || timeWindow.matches(ts)) {
if (ignoreNaN) {
boolean skip = false;
for (int i = 0; i < args.size(); i++) {
if (Double.isNaN(result[i])) {
skip = true;
break;
}
}
if (!skip) {
data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
}
} else {
data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
}
}
}
return new TbelCfTsRollingData(timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs), data);
}
@JsonIgnore @JsonIgnore
public int getSize() { public int getSize() {
return values.size(); return values.size();
@ -266,11 +360,6 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
return values.iterator(); return values.iterator();
} }
@Override
public void forEach(Consumer<? super TbelCfTsDoubleVal> action) {
values.forEach(action);
}
@Override @Override
public String getType() { public String getType() {
return "TS_ROLLING"; return "TS_ROLLING";

61
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingData.java

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2025 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.script.api.tbel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
public class TbelCfTsRollingData implements TbelCfObject, Iterable<TbelCfTsMultiDoubleVal> {
@Getter
private final TbTimeWindow timeWindow;
@Getter
private final List<TbelCfTsMultiDoubleVal> values;
public TbelCfTsRollingData(TbTimeWindow timeWindow, List<TbelCfTsMultiDoubleVal> values) {
this.timeWindow = timeWindow;
this.values = Collections.unmodifiableList(values);
}
@Override
public long memorySize() {
return 12 + values.size() * OBJ_SIZE;
}
@JsonIgnore
public List<TbelCfTsMultiDoubleVal> getValue() {
return values;
}
@JsonIgnore
public int getSize() {
return values.size();
}
@Override
public Iterator<TbelCfTsMultiDoubleVal> iterator() {
return values.iterator();
}
}

10
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -1109,7 +1109,7 @@ public class TbUtilsTest {
String validInput = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3, 4, 5}); String validInput = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3, 4, 5});
ExecutionArrayList<Byte> actual = TbUtils.base64ToBytesList(ctx, validInput); ExecutionArrayList<Byte> actual = TbUtils.base64ToBytesList(ctx, validInput);
ExecutionArrayList<Byte> expected = new ExecutionArrayList<>(ctx); ExecutionArrayList<Byte> expected = new ExecutionArrayList<>(ctx);
expected.addAll(List.of((byte) 1, (byte)2, (byte)3, (byte)4, (byte)5)); expected.addAll(List.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5));
Assertions.assertEquals(expected, actual); Assertions.assertEquals(expected, actual);
String emptyInput = Base64.getEncoder().encodeToString(new byte[]{}); String emptyInput = Base64.getEncoder().encodeToString(new byte[]{});
@ -1123,6 +1123,7 @@ public class TbUtilsTest {
TbUtils.base64ToBytesList(ctx, null); TbUtils.base64ToBytesList(ctx, null);
}); });
} }
@Test @Test
public void bytesToHex_Test() { public void bytesToHex_Test() {
byte[] bb = {(byte) 0xBB, (byte) 0xAA}; byte[] bb = {(byte) 0xBB, (byte) 0xAA};
@ -1136,6 +1137,13 @@ public class TbUtilsTest {
Assertions.assertEquals(expected, actual); Assertions.assertEquals(expected, actual);
} }
@Test
void toInt() {
Assertions.assertEquals(1729, TbUtils.toInt(doubleVal));
Assertions.assertEquals(13, TbUtils.toInt(12.8));
Assertions.assertEquals(28, TbUtils.toInt(28.0));
}
private static List<Byte> toList(byte[] data) { private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length); List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) { for (Byte b : data) {

88
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java

@ -15,10 +15,15 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thingsboard.common.util.JacksonUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -33,7 +38,7 @@ public class TbelCfTsRollingArgTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
rollingArg = new TbelCfTsRollingArg( rollingArg = new TbelCfTsRollingArg(
new TbTimeWindow(ts - 30000, ts - 10, 10), new TbTimeWindow(ts - 30000, ts - 10),
List.of( List.of(
new TbelCfTsDoubleVal(ts - 10, Double.NaN), new TbelCfTsDoubleVal(ts - 10, Double.NaN),
new TbelCfTsDoubleVal(ts - 20, 2.0), new TbelCfTsDoubleVal(ts - 20, 2.0),
@ -98,7 +103,7 @@ public class TbelCfTsRollingArgTest {
void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() { void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() {
assertThat(rollingArg.first()).isEqualTo(2.0); assertThat(rollingArg.first()).isEqualTo(2.0);
rollingArg = new TbelCfTsRollingArg( rollingArg = new TbelCfTsRollingArg(
new TbTimeWindow(ts - 30000, ts - 10, 10), new TbTimeWindow(ts - 30000, ts - 10),
List.of( List.of(
new TbelCfTsDoubleVal(ts - 10, Double.NaN), new TbelCfTsDoubleVal(ts - 10, Double.NaN),
new TbelCfTsDoubleVal(ts - 40, Double.NaN), new TbelCfTsDoubleVal(ts - 40, Double.NaN),
@ -117,7 +122,7 @@ public class TbelCfTsRollingArgTest {
@Test @Test
void testEmptyValues() { void testEmptyValues() {
rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10, 10), List.of()); rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10), List.of());
assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
@ -128,4 +133,81 @@ public class TbelCfTsRollingArgTest {
assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty."); assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
} }
@Test
public void merge_two_rolling_args_ts_match_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2);
Assertions.assertEquals(3, result.getSize());
Assertions.assertNotNull(result.getValues());
Assertions.assertNotNull(result.getValues().get(0));
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
}
@Test
public void merge_two_rolling_args_with_timewindow_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2, Collections.singletonMap("timeWindow", new TbTimeWindow(0, 10000)));
Assertions.assertEquals(2, result.getSize());
Assertions.assertNotNull(result.getValues());
Assertions.assertNotNull(result.getValues().get(0));
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
result = arg1.merge(arg2, Collections.singletonMap("timeWindow", Map.of("startTs", 0L, "endTs", 10000)));
Assertions.assertEquals(2, result.getSize());
Assertions.assertNotNull(result.getValues());
Assertions.assertNotNull(result.getValues().get(0));
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
}
@Test
public void merge_two_rolling_args_ts_mismatch_default_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2);
Assertions.assertEquals(3, result.getSize());
Assertions.assertNotNull(result.getValues());
TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
Assertions.assertNotNull(item0);
Assertions.assertEquals(200L, item0.getTs());
Assertions.assertEquals(1, item0.getValues()[0]);
Assertions.assertEquals(11, item0.getValues()[1]);
}
@Test
public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2, Collections.singletonMap("ignoreNaN", false));
Assertions.assertEquals(4, result.getSize());
Assertions.assertNotNull(result.getValues());
TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
Assertions.assertNotNull(item0);
Assertions.assertEquals(100L, item0.getTs());
Assertions.assertEquals(1, item0.getValues()[0]);
Assertions.assertEquals(Double.NaN, item0.getValues()[1]);
TbelCfTsMultiDoubleVal item1 = result.getValues().get(1);
Assertions.assertEquals(200L, item1.getTs());
Assertions.assertEquals(1, item1.getValues()[0]);
Assertions.assertEquals(11, item1.getValues()[1]);
}
} }

2
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -106,7 +106,7 @@ public interface AlarmDao extends Dao<Alarm> {
AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime); AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds);
PageData<EntitySubtype> findTenantAlarmTypes(UUID tenantId, PageLink pageLink); PageData<EntitySubtype> findTenantAlarmTypes(UUID tenantId, PageLink pageLink);

7
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -351,8 +351,13 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
@Override @Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) {
return countAlarmsByQuery(tenantId, customerId, query, null);
}
@Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds) {
validateId(tenantId, id -> INCORRECT_TENANT_ID + id); validateId(tenantId, id -> INCORRECT_TENANT_ID + id);
return alarmDao.countAlarmsByQuery(tenantId, customerId, query); return alarmDao.countAlarmsByQuery(tenantId, customerId, query, orderedEntityIds);
} }
@Override @Override

4
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -415,8 +415,8 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
} }
@Override @Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds) {
return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query); return alarmQueryRepository.countAlarmsByQuery(tenantId, customerId, query, orderedEntityIds);
} }
@Override @Override

2
dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmQueryRepository.java

@ -30,6 +30,6 @@ public interface AlarmQueryRepository {
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds); AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query); long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds);
} }

39
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java

@ -44,6 +44,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Repository @Repository
@ -314,25 +315,41 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
} }
@Override @Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query, Collection<EntityId> orderedEntityIds) {
QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM));
if (query.isSearchPropagatedAlarms()) { if (query.isSearchPropagatedAlarms()) {
ctx.append("select count(distinct(a.id)) from alarm_info a "); ctx.append("select count(distinct(a.id)) from alarm_info a ");
ctx.append(JOIN_ENTITY_ALARMS); ctx.append(JOIN_ENTITY_ALARMS);
ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId"); if (orderedEntityIds != null) {
ctx.addUuidParameter("tenantId", tenantId.getId()); if (orderedEntityIds.isEmpty()) {
if (customerId != null && !customerId.isNullUid()) { return 0;
ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId"); }
ctx.addUuidParameter("customerId", customerId.getId()); ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
ctx.append("where ea.entity_id in (:entity_filter_entity_ids)");
} else {
ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId");
ctx.addUuidParameter("tenantId", tenantId.getId());
if (customerId != null && !customerId.isNullUid()) {
ctx.append(" and a.customer_id = :customerId and ea.customer_id = :customerId");
ctx.addUuidParameter("customerId", customerId.getId());
}
} }
} else { } else {
ctx.append("select count(id) from alarm_info a "); ctx.append("select count(id) from alarm_info a ");
ctx.append("where a.tenant_id = :tenantId"); if (orderedEntityIds != null) {
ctx.addUuidParameter("tenantId", tenantId.getId()); if (orderedEntityIds.isEmpty()) {
if (customerId != null && !customerId.isNullUid()) { return 0;
ctx.append(" and a.customer_id = :customerId"); }
ctx.addUuidParameter("customerId", customerId.getId()); ctx.addUuidListParameter("entity_filter_entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
ctx.append("where a.originator_id in (:entity_filter_entity_ids)");
} else {
ctx.append("where a.tenant_id = :tenantId");
ctx.addUuidParameter("tenantId", tenantId.getId());
if (customerId != null && !customerId.isNullUid()) {
ctx.append(" and a.customer_id = :customerId");
ctx.addUuidParameter("customerId", customerId.getId());
}
} }
} }

51
dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java

@ -22,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
@ -48,6 +49,7 @@ import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityDataSortOrder; import org.thingsboard.server.common.data.query.EntityDataSortOrder;
import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.EntityListFilter;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
@ -936,4 +938,53 @@ public class AlarmServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, alarms.getData().size()); Assert.assertEquals(0, alarms.getData().size());
} }
@Test
public void testCountAlarmsForEntities() throws ExecutionException, InterruptedException {
AssetId parentId = new AssetId(Uuids.timeBased());
AssetId childId = new AssetId(Uuids.timeBased());
EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get());
long ts = System.currentTimeMillis();
AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(tenantId)
.originator(childId)
.type(TEST_ALARM)
.severity(AlarmSeverity.CRITICAL)
.startTs(ts).build());
AlarmInfo created = result.getAlarm();
created.setPropagate(true);
result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created));
created = result.getAlarm();
EntityListFilter entityListFilter = new EntityListFilter();
entityListFilter.setEntityList(List.of(childId.getId().toString(), parentId.getId().toString()));
entityListFilter.setEntityType(EntityType.ASSET);
AlarmCountQuery countQuery = new AlarmCountQuery(entityListFilter);
countQuery.setStartTs(0L);
countQuery.setEndTs(System.currentTimeMillis());
long alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));
Assert.assertEquals(1, alarmsCount);
countQuery.setSearchPropagatedAlarms(true);
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(parentId));
Assert.assertEquals(1, alarmsCount);
created = alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm();
countQuery.setStatusList(List.of(AlarmSearchStatus.UNACK));
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));
Assert.assertEquals(0, alarmsCount);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null);
countQuery.setStatusList(List.of(AlarmSearchStatus.CLEARED));
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));
Assert.assertEquals(1, alarmsCount);
}
} }

5
ui-ngx/src/app/core/http/calculated-fields.service.ts

@ -23,6 +23,7 @@ import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/m
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models'; import { EntityTestScriptResult } from '@shared/models/entity.models';
import { CalculatedFieldEventBody } from '@shared/models/event.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -53,4 +54,8 @@ export class CalculatedFieldsService {
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> { public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
return this.http.post<EntityTestScriptResult>('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config)); return this.http.post<EntityTestScriptResult>('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config));
} }
public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable<CalculatedFieldEventBody> {
return this.http.get<CalculatedFieldEventBody>(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config));
}
} }

21
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -35,10 +35,11 @@ import {
import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants'; import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { map, startWith } from 'rxjs/operators'; import { map, startWith, switchMap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models'; import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'tb-calculated-field-dialog', selector: 'tb-calculated-field-dialog',
@ -136,7 +137,23 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
} }
onTestScript(): void { onTestScript(): void {
this.data.getTestScriptDialogFn(this.fromGroupValue, null, false).subscribe(expression => { const calculatedFieldId = this.data.value?.id?.id;
let testScriptDialogResult$: Observable<string>;
if (calculatedFieldId) {
testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId)
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false);
}),
takeUntilDestroyed(this.destroyRef)
)
} else {
testScriptDialogResult$ = this.data.getTestScriptDialogFn(this.fromGroupValue, null, false);
}
testScriptDialogResult$.subscribe(expression => {
this.configFormGroup.get('expressionSCRIPT').setValue(expression); this.configFormGroup.get('expressionSCRIPT').setValue(expression);
this.configFormGroup.get('expressionSCRIPT').markAsDirty(); this.configFormGroup.get('expressionSCRIPT').markAsDirty();
}); });

9
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.html

@ -16,6 +16,15 @@
--> -->
<ng-container [formGroup]="alarmCountWidgetConfigForm"> <ng-container [formGroup]="alarmCountWidgetConfigForm">
<tb-datasources
[configMode]="basicMode"
hideDatasourcesMode
hideDatasourceLabel
hideDataKeys
hideAlarmFilter
displayDatasourceFilterForBasicMode
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title" translate>alarm.filter</div> <div class="tb-form-panel-title" translate>alarm.filter</div>

2
ui-ngx/src/app/modules/home/components/widget/config/basic/alarm/alarm-count-basic-config.component.ts

@ -69,6 +69,7 @@ export class AlarmCountBasicConfigComponent extends BasicWidgetConfigComponent {
const settings: CountWidgetSettings = {...countDefaultSettings(true), ...(configData.config.settings || {})}; const settings: CountWidgetSettings = {...countDefaultSettings(true), ...(configData.config.settings || {})};
this.alarmCountWidgetConfigForm = this.fb.group({ this.alarmCountWidgetConfigForm = this.fb.group({
alarmFilterConfig: [getAlarmFilterConfig(configData.config.datasources), []], alarmFilterConfig: [getAlarmFilterConfig(configData.config.datasources), []],
datasources: [configData.config.datasources, []],
settings: [settings, []], settings: [settings, []],
@ -81,6 +82,7 @@ export class AlarmCountBasicConfigComponent extends BasicWidgetConfigComponent {
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.datasources = config.datasources;
setAlarmFilterConfig(config.alarmFilterConfig, this.widgetConfig.config.datasources); setAlarmFilterConfig(config.alarmFilterConfig, this.widgetConfig.config.datasources);
this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings}; this.widgetConfig.config.settings = {...(this.widgetConfig.config.settings || {}), ...config.settings};

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

@ -36,7 +36,7 @@
datasourceFormGroup.get('type').value === datasourceType.entity || datasourceFormGroup.get('type').value === datasourceType.entity ||
datasourceFormGroup.get('type').value === datasourceType.entityCount || datasourceFormGroup.get('type').value === datasourceType.entityCount ||
datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''"> datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''">
<tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount" <tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount && !hideAlarmFilter"
propagatedFilter="false" propagatedFilter="false"
[initialAlarmFilterConfig]="{ statusList: [alarmSearchStatus.ACTIVE] }" [initialAlarmFilterConfig]="{ statusList: [alarmSearchStatus.ACTIVE] }"
style="height: 56px; margin-bottom: 22px;" style="height: 56px; margin-bottom: 22px;"
@ -47,9 +47,9 @@
formControlName="deviceId"> formControlName="deviceId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
<tb-entity-alias-select <tb-entity-alias-select
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount" *ngIf="datasourceFormGroup.get('type').value !== datasourceType.device"
[showLabel]="true" [showLabel]="true"
[tbRequired]="!datasourcesOptional" [tbRequired]="!entityAliasOptional"
[aliasController]="aliasController" [aliasController]="aliasController"
formControlName="entityAliasId" formControlName="entityAliasId"
[callbacks]="entityAliasSelectCallbacks"> [callbacks]="entityAliasSelectCallbacks">
@ -98,7 +98,7 @@
</tb-data-keys> </tb-data-keys>
</section> </section>
<tb-filter-select <tb-filter-select
*ngIf="(!basicMode || displayDatasourceFilterForBasicMode) && ![datasourceType.function, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)" *ngIf="(!basicMode || displayDatasourceFilterForBasicMode) && ![datasourceType.function].includes(datasourceFormGroup.get('type').value)"
[showLabel]="true" [showLabel]="true"
[aliasController]="aliasController" [aliasController]="aliasController"
formControlName="filterId" formControlName="filterId"

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

@ -106,6 +106,11 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional; return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional;
} }
public get entityAliasOptional(): boolean {
const type: DatasourceType = this.datasourceFormGroup.get('type').value;
return this.datasourcesOptional || type === DatasourceType.alarmCount
}
public get maxDataKeys(): number { public get maxDataKeys(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys; return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys;
} }
@ -170,6 +175,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.datasourcesComponent?.hideLatestDataKeys; return this.datasourcesComponent?.hideLatestDataKeys;
} }
public get hideAlarmFilter(): boolean {
return this.datasourcesComponent?.hideAlarmFilter;
}
@Input() @Input()
disabled: boolean; disabled: boolean;

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

@ -131,6 +131,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean() @coerceBoolean()
hideLatestDataKeys = false; hideLatestDataKeys = false;
@Input()
@coerceBoolean()
hideAlarmFilter = false;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
forceSingleDatasource = false; forceSingleDatasource = false;

89
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -272,7 +272,7 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = {
export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = { export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
max: { max: {
meta: 'function', meta: 'function',
description: 'Computes the maximum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', description: 'Returns the maximum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -288,7 +288,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
}, },
min: { min: {
meta: 'function', meta: 'function',
description: 'Computes the minimum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', description: 'Returns the minimum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -304,7 +304,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
}, },
mean: { mean: {
meta: 'function', meta: 'function',
description: 'Computes the mean value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.', description: 'Computes the mean value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -318,9 +318,25 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
type: 'number' type: 'number'
} }
}, },
avg: {
meta: 'function',
description: 'Computes the average value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The average value, or NaN if applicable',
type: 'number'
}
},
std: { std: {
meta: 'function', meta: 'function',
description: 'Computes the standard deviation in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', description: 'Computes the standard deviation of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -336,7 +352,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
}, },
median: { median: {
meta: 'function', meta: 'function',
description: 'Computes the median value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.', description: 'Computes the median value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -352,7 +368,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
}, },
count: { count: {
meta: 'function', meta: 'function',
description: 'Counts values in the list of rolling argument values. Counts non-NaN values if ignoreNaN is true, otherwise - total size.', description: 'Counts values of the rolling argument. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -368,7 +384,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
}, },
last: { last: {
meta: 'function', meta: 'function',
description: 'Returns the last non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the last value.', description: 'Returns the last non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the last value.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -384,7 +400,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
}, },
first: { first: {
meta: 'function', meta: 'function',
description: 'Returns the first non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the first value.', description: 'Returns the first non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the first value.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -400,7 +416,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
}, },
sum: { sum: {
meta: 'function', meta: 'function',
description: 'Computes the sum of values in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.', description: 'Computes the sum of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [ args: [
{ {
name: 'ignoreNaN', name: 'ignoreNaN',
@ -413,12 +429,56 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
description: 'The sum of values, or NaN if applicable', description: 'The sum of values, or NaN if applicable',
type: 'number' type: 'number'
} }
},
merge: {
meta: 'function',
description: 'Merges current object with other time series rolling argument into a single object by aligning their timestamped values. Supports optional configurable settings.',
args: [
{
name: 'other',
description: "A time series rolling argument to be merged with the current object.",
type: "object",
optional: true
},
{
name: "settings",
description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
type: "object",
optional: true
}
],
return: {
description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
}
},
mergeAll: {
meta: 'function',
description: 'Merges current object with other time series rolling arguments into a single object by aligning their timestamped values. Supports optional configurable settings.',
args: [
{
name: 'others',
description: "A list of time series rolling arguments to be merged with the current object.",
type: "object[]",
optional: true
},
{
name: "settings",
description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
type: "object",
optional: true
}
],
return: {
description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
}
} }
}; };
export const CalculatedFieldRollingValueArgumentAutocomplete = { export const CalculatedFieldRollingValueArgumentAutocomplete = {
meta: 'object', meta: 'object',
type: '{ values: { ts: number; value: any; }[]; timeWindow: { startTs: number; endTs: number; limit: number } }; }', type: '{ values: { ts: number; value: number; }[]; timeWindow: { startTs: number; endTs: number } }; }',
description: 'Calculated field rolling value argument.', description: 'Calculated field rolling value argument.',
children: { children: {
...CalculatedFieldRollingValueArgumentFunctionsAutocomplete, ...CalculatedFieldRollingValueArgumentFunctionsAutocomplete,
@ -429,7 +489,7 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
}, },
timeWindow: { timeWindow: {
meta: 'object', meta: 'object',
type: '{ startTs: number; endTs: number; limit: number }', type: '{ startTs: number; endTs: number }',
description: 'Time window configuration', description: 'Time window configuration',
children: { children: {
startTs: { startTs: {
@ -441,11 +501,6 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
meta: 'number', meta: 'number',
type: 'number', type: 'number',
description: 'End time stamp', description: 'End time stamp',
},
limit: {
meta: 'number',
type: 'number',
description: 'Limit',
} }
} }
} }
@ -504,7 +559,7 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = {
} }
const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array<AceHighlightRule> = const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array<AceHighlightRule> =
['max', 'min', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum'].map(funcName => ({ ['max', 'min', 'avg', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum', 'merge', 'mergeAll'].map(funcName => ({
token: 'tb.calculated-field-func', token: 'tb.calculated-field-func',
regex: `\\b${funcName}\\b`, regex: `\\b${funcName}\\b`,
next: 'no_regex' next: 'no_regex'

10
ui-ngx/src/app/shared/models/tenant.model.ts

@ -156,11 +156,11 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
rpcTtlDays: 0, rpcTtlDays: 0,
queueStatsTtlDays: 0, queueStatsTtlDays: 0,
ruleEngineExceptionsTtlDays: 0, ruleEngineExceptionsTtlDays: 0,
maxCalculatedFieldsPerEntity: 0, maxCalculatedFieldsPerEntity: 5,
maxArgumentsPerCF: 0, maxArgumentsPerCF: 10,
maxDataPointsPerRollingArg: 0, maxDataPointsPerRollingArg: 1000,
maxStateSizeInKBytes: 0, maxStateSizeInKBytes: 32,
maxSingleValueArgumentSizeInKBytes: 0, maxSingleValueArgumentSizeInKBytes: 2,
calculatedFieldDebugEventsRateLimit: '' calculatedFieldDebugEventsRateLimit: ''
}; };
configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT}; configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT};

Loading…
Cancel
Save