From b9952d7d27db95d8237bdb5d7405021d29f5c5f0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 2 Dec 2025 16:36:55 +0200 Subject: [PATCH 01/21] added produceIntermediateResult flag to handle updates during the current interval and changed default value for min deduplication interval --- .../main/data/upgrade/basic/schema_update.sql | 8 +++-- .../controller/SystemInfoController.java | 1 + .../controller/TenantProfileController.java | 7 +++-- .../cf/ctx/state/CalculatedFieldCtx.java | 9 ++++++ ...EntityAggregationCalculatedFieldState.java | 29 ++++++++++++++++++- .../src/main/resources/thingsboard.yml | 2 +- .../server/common/data/SystemParams.java | 1 + ...gregationCalculatedFieldConfiguration.java | 1 + .../DefaultTenantProfileConfiguration.java | 6 ++-- .../CalculatedFieldDataValidator.java | 4 +-- ui-ngx/src/app/shared/models/tenant.model.ts | 2 +- 11 files changed, 57 insertions(+), 13 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 94b1a8b878..9091b603fd 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -24,8 +24,9 @@ SET profile_data = jsonb_set( 'minAllowedScheduledUpdateIntervalInSecForCF', 60, 'maxRelationLevelPerCfArgument', 10, 'maxRelatedEntitiesToReturnPerCfArgument', 100, - 'minAllowedDeduplicationIntervalInSecForCF', 60, - 'minAllowedAggregationIntervalInSecForCF', 60 + 'minAllowedDeduplicationIntervalInSecForCF', 10, + 'minAllowedAggregationIntervalInSecForCF', 60, + 'minAllowedRealtimeAggregationIntervalInSecForCF', 300 ) || jsonb_strip_nulls(profile_data -> 'configuration') @@ -36,7 +37,8 @@ WHERE NOT ( 'maxRelationLevelPerCfArgument', 'maxRelatedEntitiesToReturnPerCfArgument', 'minAllowedDeduplicationIntervalInSecForCF', - 'minAllowedAggregationIntervalInSecForCF' + 'minAllowedAggregationIntervalInSecForCF', + 'minAllowedRealtimeAggregationIntervalInSecForCF' ] ); diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 9c04cb92bd..2c0ce1c938 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -166,6 +166,7 @@ public class SystemInfoController extends BaseController { systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); + systemParams.setMinAllowedRealtimeAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedRealtimeAggregationIntervalInSecForCF()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 19cc2341ad..9c3985d3b0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -166,9 +166,10 @@ public class TenantProfileController extends BaseController { " \"maxRelatedEntitiesToReturnPerCfArgument\": 100,\n" + " \"maxDataPointsPerRollingArg\": 1000,\n" + " \"maxStateSizeInKBytes\": 32,\n" + - " \"maxSingleValueArgumentSizeInKBytes\": 2" + - " \"minAllowedDeduplicationIntervalInSecForCF\": 60" + - " \"minAllowedAggregationIntervalInSecForCF\": 60" + + " \"maxSingleValueArgumentSizeInKBytes\": 2," + + " \"minAllowedDeduplicationIntervalInSecForCF\": 10," + + " \"minAllowedAggregationIntervalInSecForCF\": 60," + + " \"minAllowedRealtimeAggregationIntervalInSecForCF\": 300" + " }\n" + " },\n" + " \"default\": false\n" + diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index f04f3b109a..ec2d11ebf9 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -115,6 +115,7 @@ public class CalculatedFieldCtx implements Closeable { private long maxStateSize; private long maxSingleValueArgumentSize; + private long realtimeAggregationIntervalMillis; private boolean relationQueryDynamicArguments; private List mainEntityGeofencingArgumentNames; @@ -210,6 +211,7 @@ public class CalculatedFieldCtx implements Closeable { this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); } public boolean requiresScheduledReevaluation() { @@ -223,6 +225,12 @@ public class CalculatedFieldCtx implements Closeable { lastReevaluationTs = now; return true; } + if (entityAggregationConfig.isProduceIntermediateResult()) { + if (now - lastReevaluationTs >= realtimeAggregationIntervalMillis) { + lastReevaluationTs = now; + return true; + } + } ZonedDateTime lastReevaluationTime = TimeUtils.toZonedDateTime(lastReevaluationTs, entityAggregationConfig.getInterval().getZoneId()); long previousIntervalEndTs = entityAggregationConfig.getInterval().getDateTimeIntervalEndTs(lastReevaluationTime); if (now >= previousIntervalEndTs) { @@ -291,6 +299,7 @@ public class CalculatedFieldCtx implements Closeable { public void updateTenantProfileProperties() { this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); } public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index f3c3e8a1cc..a722f688dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -63,6 +63,8 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt private long checkInterval; private Map metrics; + private boolean produceIntermediateResult; + private EntityAggregationDebugArgumentsTracker debugTracker; private CalculatedFieldProcessingService cfProcessingService; @@ -81,6 +83,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt checkInterval = TimeUnit.SECONDS.toMillis(ctx.getSystemContext().getCfCheckInterval()); interval = configuration.getInterval(); metrics = configuration.getMetrics(); + produceIntermediateResult = configuration.isProduceIntermediateResult(); } @Override @@ -113,7 +116,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt Map> results = new HashMap<>(); List expiredIntervals = new ArrayList<>(); getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { - processInterval(now, intervalEntry, argIntervalStatuses, expiredIntervals, results); + processInterval(now, ctx, intervalEntry, argIntervalStatuses, expiredIntervals, results); }); removeExpiredIntervals(expiredIntervals); @@ -193,6 +196,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt } private void processInterval(long now, + CalculatedFieldCtx ctx, AggIntervalEntry intervalEntry, Map args, List expiredIntervals, @@ -208,6 +212,10 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt if (watermarkDuration == 0) { expiredIntervals.add(intervalEntry); } + } else if (now - startTs < intervalEntry.getIntervalDuration()) { + if (produceIntermediateResult) { + handleCurrentInterval(ctx, intervalEntry, args, results); + } } } @@ -242,6 +250,25 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt }); } + private void handleCurrentInterval(CalculatedFieldCtx ctx, + AggIntervalEntry intervalEntry, + Map args, + Map> results) { + long realtimeAggregationInterval = ctx.getRealtimeAggregationIntervalMillis(); + args.forEach((argName, argEntryIntervalStatus) -> { + if (argEntryIntervalStatus.intervalPassed(realtimeAggregationInterval)) { + if (argEntryIntervalStatus.argsUpdated()) { + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + argEntryIntervalStatus.setLastArgsRefreshTs(-1); + processArgument(intervalEntry, argName, false, results); + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) {// TODO: should we return default value when the interval has not ended + argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); + processArgument(intervalEntry, argName, true, results); + } + } + }); + } + private void processArgument(AggIntervalEntry intervalEntry, String argName, boolean useDefault, diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d3d04bcea0..8dab52f100 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -541,7 +541,7 @@ actors: # Interval in seconds to check calculated fields for re-evaluation interval. 1 minute by default. check_interval: "${ACTORS_CALCULATED_FIELDS_CHECK_INTERVAL_SEC:60}" alarms: - # Interval in seconds to re-evaluate Alarm rules that have a time schedule. 2 minutes by default. + # Interval in seconds to re-evaluate Alarm rules that have a time schedule. 1 minute by default. reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:60}" debug: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 0fa9b2dd78..d1b8bb0562 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -42,5 +42,6 @@ public class SystemParams { int maxRelationLevelPerCfArgument; long minAllowedDeduplicationIntervalInSecForCF; long minAllowedAggregationIntervalInSecForCF; + long minAllowedRealtimeAggregationIntervalInSecForCF; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java index f6095d41a7..488db86870 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfiguration.java @@ -43,6 +43,7 @@ public class EntityAggregationCalculatedFieldConfiguration implements ArgumentsB private AggInterval interval; @Valid private Watermark watermark; + private boolean produceIntermediateResult; @Valid @NotNull private Output output; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 87fa4a85da..3825c3c264 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -190,10 +190,12 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxStateSizeInKBytes = 32; @Schema(example = "2") private long maxSingleValueArgumentSizeInKBytes = 2; - @Schema(example = "60") - private long minAllowedDeduplicationIntervalInSecForCF = 60; + @Schema(example = "10") + private long minAllowedDeduplicationIntervalInSecForCF = 10; @Schema(example = "60") private long minAllowedAggregationIntervalInSecForCF = 60; + @Schema(example = "300") + private long minAllowedRealtimeAggregationIntervalInSecForCF = 300; @Override public long getProfileThreshold(ApiUsageRecordKey key) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index c10da4e6c6..6c333f36ad 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -50,7 +50,7 @@ public class CalculatedFieldDataValidator extends DataValidator validateCalculatedFieldConfiguration(calculatedField); validateSchedulingConfiguration(tenantId, calculatedField); validateRelationQuerySourceArguments(tenantId, calculatedField); - validateAggregationConfiguration(tenantId, calculatedField); + validateRelatedAggregationConfiguration(tenantId, calculatedField); validateEntityAggregationConfiguration(tenantId, calculatedField); } @@ -119,7 +119,7 @@ public class CalculatedFieldDataValidator extends DataValidator wrapAsDataValidation(() -> relationQueryDynamicSourceConfiguration.validateMaxRelationLevel(argumentName, maxRelationLevel))); } - private void validateAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { + private void validateRelatedAggregationConfiguration(TenantId tenantId, CalculatedField calculatedField) { if (!(calculatedField.getConfiguration() instanceof RelatedEntitiesAggregationCalculatedFieldConfiguration aggConfiguration)) { return; } diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 0cfa8df888..b8f04250ce 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -176,7 +176,7 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan maxArgumentsPerCF: 10, maxDataPointsPerRollingArg: 1000, maxRelationLevelPerCfArgument: 10, - minAllowedDeduplicationIntervalInSecForCF: 60, + minAllowedDeduplicationIntervalInSecForCF: 10, minAllowedAggregationIntervalInSecForCF: 60, maxRelatedEntitiesToReturnPerCfArgument: 100, minAllowedScheduledUpdateIntervalInSecForCF: 0, From 70c10ce28b46752b130cefdfee1651d2b1e80a73 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 3 Dec 2025 08:17:58 +0200 Subject: [PATCH 02/21] moved check reevaluation interval to tenant profile config --- .../server/actors/ActorSystemContext.java | 8 ------- ...alculatedFieldManagerMessageProcessor.java | 18 +++++++++++---- .../controller/SystemInfoController.java | 2 ++ .../cf/ctx/state/CalculatedFieldCtx.java | 23 +++++++++++++------ ...EntityAggregationCalculatedFieldState.java | 11 ++++----- .../src/main/resources/thingsboard.yml | 5 ---- .../server/common/data/SystemParams.java | 2 ++ .../DefaultTenantProfileConfiguration.java | 4 ++++ 8 files changed, 42 insertions(+), 31 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 654ae8bf29..9848ac2fe6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -666,14 +666,6 @@ public class ActorSystemContext { @Getter private long cfCalculationResultTimeout; - @Value("${actors.calculated_fields.check_interval:60}") - @Getter - private long cfCheckInterval; - - @Value("${actors.alarms.reevaluation_interval:60}") - @Getter - private long alarmRulesReevaluationInterval; - @Autowired @Getter private MqttClientSettings mqttClientSettings; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index b19ad1a8b4..eca0d9447f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; @@ -144,10 +145,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); - if (cfsReevaluationTask != null) { - cfsReevaluationTask.cancel(true); - cfsReevaluationTask = null; - } + cancelReevaluationTask(); ctx.stop(ctx.getSelf()); } @@ -177,6 +175,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void scheduleCfsReevaluation() { + long cfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { try { calculatedFields.values().forEach(cf -> { @@ -190,7 +189,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } catch (Exception e) { log.warn("[{}] Failed to trigger CFs reevaluation", tenantId, e); } - }, systemContext.getCfCheckInterval(), systemContext.getCfCheckInterval(), TimeUnit.SECONDS); + }, cfCheckInterval, cfCheckInterval, TimeUnit.SECONDS); } public void onEntityLifecycleMsg(CalculatedFieldEntityLifecycleMsg msg) throws CalculatedFieldException { @@ -257,6 +256,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) { + cancelReevaluationTask(); + scheduleCfsReevaluation(); Stream.concat( calculatedFields.values().stream(), entityIdCalculatedFields.values().stream().flatMap(Collection::stream) @@ -875,4 +876,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } + private void cancelReevaluationTask() { + if (cfsReevaluationTask != null) { + cfsReevaluationTask.cancel(true); + cfsReevaluationTask = null; + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 2c0ce1c938..175b8403ad 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -167,6 +167,8 @@ public class SystemInfoController extends BaseController { systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); systemParams.setMinAllowedRealtimeAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedRealtimeAggregationIntervalInSecForCF()); + systemParams.setCfReevaluationCheckInterval(tenantProfileConfiguration.getCfReevaluationCheckInterval()); + systemParams.setAlarmsReevaluationInterval(tenantProfileConfiguration.getAlarmsReevaluationInterval()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index ec2d11ebf9..d0f35a8571 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.util.TimeUtils; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; @@ -123,6 +124,8 @@ public class CalculatedFieldCtx implements Closeable { private List relatedEntityArgumentNames; private long scheduledUpdateIntervalMillis; + private long cfCheckReevaluationInterval; + private long alarmReevaluationInterval; private Argument propagationArgument; private boolean applyExpressionForResolvedArguments; @@ -209,9 +212,12 @@ public class CalculatedFieldCtx implements Closeable { this.alarmService = systemContext.getAlarmService(); this.cfProcessingService = systemContext.getCalculatedFieldProcessingService(); - this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; - this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); + ApiLimitService apiLimitService = systemContext.getApiLimitService(); + this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; + this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); + this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); + this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); } public boolean requiresScheduledReevaluation() { @@ -241,7 +247,7 @@ public class CalculatedFieldCtx implements Closeable { boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { if (requiresScheduledReevaluation) { - long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); + long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(alarmReevaluationInterval); if (now - lastReevaluationTs >= reevaluationIntervalMillis) { lastReevaluationTs = now; return true; @@ -297,9 +303,12 @@ public class CalculatedFieldCtx implements Closeable { } public void updateTenantProfileProperties() { - this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; - this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); + ApiLimitService apiLimitService = systemContext.getApiLimitService(); + this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; + this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; + this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); + this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); + this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); } public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index a722f688dc..b752950a15 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -60,7 +60,6 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt private AggInterval interval; private long watermarkDuration; - private long checkInterval; private Map metrics; private boolean produceIntermediateResult; @@ -80,7 +79,6 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt var configuration = (EntityAggregationCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); Watermark watermark = configuration.getWatermark(); watermarkDuration = watermark == null ? 0 : TimeUnit.SECONDS.toMillis(watermark.getDuration()); - checkInterval = TimeUnit.SECONDS.toMillis(ctx.getSystemContext().getCfCheckInterval()); interval = configuration.getInterval(); metrics = configuration.getMetrics(); produceIntermediateResult = configuration.isProduceIntermediateResult(); @@ -208,7 +206,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt handleExpiredInterval(intervalEntry, args, results); expiredIntervals.add(intervalEntry); } else if (now - startTs >= intervalEntry.getIntervalDuration()) { - handleActiveInterval(intervalEntry, args, results); + handleActiveInterval(ctx, intervalEntry, args, results); if (watermarkDuration == 0) { expiredIntervals.add(intervalEntry); } @@ -233,11 +231,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt }); } - private void handleActiveInterval(AggIntervalEntry intervalEntry, + private void handleActiveInterval(CalculatedFieldCtx ctx, + AggIntervalEntry intervalEntry, Map args, Map> results) { args.forEach((argName, argEntryIntervalStatus) -> { - if (argEntryIntervalStatus.intervalPassed(checkInterval)) { + if (argEntryIntervalStatus.intervalPassed(ctx.getCfCheckReevaluationInterval())) { if (argEntryIntervalStatus.argsUpdated()) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastArgsRefreshTs(-1); @@ -261,7 +260,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastArgsRefreshTs(-1); processArgument(intervalEntry, argName, false, results); - } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) {// TODO: should we return default value when the interval has not ended + } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); processArgument(intervalEntry, argName, true, results); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 8dab52f100..66669a8280 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -538,11 +538,6 @@ actors: 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}" - # Interval in seconds to check calculated fields for re-evaluation interval. 1 minute by default. - check_interval: "${ACTORS_CALCULATED_FIELDS_CHECK_INTERVAL_SEC:60}" - alarms: - # Interval in seconds to re-evaluate Alarm rules that have a time schedule. 1 minute by default. - reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:60}" debug: settings: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index d1b8bb0562..37ce35fa84 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -43,5 +43,7 @@ public class SystemParams { long minAllowedDeduplicationIntervalInSecForCF; long minAllowedAggregationIntervalInSecForCF; long minAllowedRealtimeAggregationIntervalInSecForCF; + long cfReevaluationCheckInterval; + long alarmsReevaluationInterval; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 3825c3c264..5291ed7a7a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -196,6 +196,10 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long minAllowedAggregationIntervalInSecForCF = 60; @Schema(example = "300") private long minAllowedRealtimeAggregationIntervalInSecForCF = 300; + @Schema(example = "60") + private long cfReevaluationCheckInterval = 60; + @Schema(example = "60") + private long alarmsReevaluationInterval = 60; @Override public long getProfileThreshold(ApiUsageRecordKey key) { From 9c6ab729150e81d5eac1eafc8f108f826de66da1 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 3 Dec 2025 09:03:37 +0200 Subject: [PATCH 03/21] minor refactoring --- .../main/data/upgrade/basic/schema_update.sql | 8 +++-- .../controller/SystemInfoController.java | 2 +- .../controller/TenantProfileController.java | 4 ++- .../cf/ctx/state/CalculatedFieldCtx.java | 8 ++--- ...EntityAggregationCalculatedFieldState.java | 31 +++---------------- .../server/common/data/SystemParams.java | 2 +- .../DefaultTenantProfileConfiguration.java | 2 +- 7 files changed, 21 insertions(+), 36 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 9091b603fd..091da4d4fa 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -26,7 +26,9 @@ SET profile_data = jsonb_set( 'maxRelatedEntitiesToReturnPerCfArgument', 100, 'minAllowedDeduplicationIntervalInSecForCF', 10, 'minAllowedAggregationIntervalInSecForCF', 60, - 'minAllowedRealtimeAggregationIntervalInSecForCF', 300 + 'minAllowedIntermediateAggregationIntervalInSecForCF', 300, + 'cfReevaluationCheckInterval', 60, + 'alarmsReevaluationInterval', 60 ) || jsonb_strip_nulls(profile_data -> 'configuration') @@ -38,7 +40,9 @@ WHERE NOT ( 'maxRelatedEntitiesToReturnPerCfArgument', 'minAllowedDeduplicationIntervalInSecForCF', 'minAllowedAggregationIntervalInSecForCF', - 'minAllowedRealtimeAggregationIntervalInSecForCF' + 'minAllowedIntermediateAggregationIntervalInSecForCF', + 'cfReevaluationCheckInterval', + 'alarmsReevaluationInterval' ] ); diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 175b8403ad..581f51d370 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -166,7 +166,7 @@ public class SystemInfoController extends BaseController { systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); - systemParams.setMinAllowedRealtimeAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedRealtimeAggregationIntervalInSecForCF()); + systemParams.setMinAllowedIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedIntermediateAggregationIntervalInSecForCF()); systemParams.setCfReevaluationCheckInterval(tenantProfileConfiguration.getCfReevaluationCheckInterval()); systemParams.setAlarmsReevaluationInterval(tenantProfileConfiguration.getAlarmsReevaluationInterval()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 9c3985d3b0..ee6d67209a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -169,7 +169,9 @@ public class TenantProfileController extends BaseController { " \"maxSingleValueArgumentSizeInKBytes\": 2," + " \"minAllowedDeduplicationIntervalInSecForCF\": 10," + " \"minAllowedAggregationIntervalInSecForCF\": 60," + - " \"minAllowedRealtimeAggregationIntervalInSecForCF\": 300" + + " \"minAllowedIntermediateAggregationIntervalInSecForCF\": 300," + + " \"cfReevaluationCheckInterval\": 60," + + " \"alarmsReevaluationInterval\": 60" + " }\n" + " },\n" + " \"default\": false\n" + diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index d0f35a8571..667f52e9c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -116,7 +116,7 @@ public class CalculatedFieldCtx implements Closeable { private long maxStateSize; private long maxSingleValueArgumentSize; - private long realtimeAggregationIntervalMillis; + private long intermediateAggregationIntervalMillis; private boolean relationQueryDynamicArguments; private List mainEntityGeofencingArgumentNames; @@ -215,7 +215,7 @@ public class CalculatedFieldCtx implements Closeable { ApiLimitService apiLimitService = systemContext.getApiLimitService(); this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; - this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); + this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedIntermediateAggregationIntervalInSecForCF)); this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); } @@ -232,7 +232,7 @@ public class CalculatedFieldCtx implements Closeable { return true; } if (entityAggregationConfig.isProduceIntermediateResult()) { - if (now - lastReevaluationTs >= realtimeAggregationIntervalMillis) { + if (now - lastReevaluationTs >= intermediateAggregationIntervalMillis) { lastReevaluationTs = now; return true; } @@ -306,7 +306,7 @@ public class CalculatedFieldCtx implements Closeable { ApiLimitService apiLimitService = systemContext.getApiLimitService(); this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; - this.realtimeAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedRealtimeAggregationIntervalInSecForCF)); + this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedIntermediateAggregationIntervalInSecForCF)); this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index b752950a15..c39aa46235 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -206,14 +206,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt handleExpiredInterval(intervalEntry, args, results); expiredIntervals.add(intervalEntry); } else if (now - startTs >= intervalEntry.getIntervalDuration()) { - handleActiveInterval(ctx, intervalEntry, args, results); + handleActiveInterval(ctx.getCfCheckReevaluationInterval(), intervalEntry, args, results); if (watermarkDuration == 0) { expiredIntervals.add(intervalEntry); } - } else if (now - startTs < intervalEntry.getIntervalDuration()) { - if (produceIntermediateResult) { - handleCurrentInterval(ctx, intervalEntry, args, results); - } + } else if (produceIntermediateResult) { + handleActiveInterval(ctx.getIntermediateAggregationIntervalMillis(), intervalEntry, args, results); } } @@ -231,31 +229,12 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt }); } - private void handleActiveInterval(CalculatedFieldCtx ctx, + private void handleActiveInterval(long cfCheckInterval, AggIntervalEntry intervalEntry, Map args, Map> results) { args.forEach((argName, argEntryIntervalStatus) -> { - if (argEntryIntervalStatus.intervalPassed(ctx.getCfCheckReevaluationInterval())) { - if (argEntryIntervalStatus.argsUpdated()) { - argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); - argEntryIntervalStatus.setLastArgsRefreshTs(-1); - processArgument(intervalEntry, argName, false, results); - } else if (argEntryIntervalStatus.getLastMetricsEvalTs() == -1) { - argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); - processArgument(intervalEntry, argName, true, results); - } - } - }); - } - - private void handleCurrentInterval(CalculatedFieldCtx ctx, - AggIntervalEntry intervalEntry, - Map args, - Map> results) { - long realtimeAggregationInterval = ctx.getRealtimeAggregationIntervalMillis(); - args.forEach((argName, argEntryIntervalStatus) -> { - if (argEntryIntervalStatus.intervalPassed(realtimeAggregationInterval)) { + if (argEntryIntervalStatus.intervalPassed(cfCheckInterval)) { if (argEntryIntervalStatus.argsUpdated()) { argEntryIntervalStatus.setLastMetricsEvalTs(System.currentTimeMillis()); argEntryIntervalStatus.setLastArgsRefreshTs(-1); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 37ce35fa84..52fa1760de 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -42,7 +42,7 @@ public class SystemParams { int maxRelationLevelPerCfArgument; long minAllowedDeduplicationIntervalInSecForCF; long minAllowedAggregationIntervalInSecForCF; - long minAllowedRealtimeAggregationIntervalInSecForCF; + long minAllowedIntermediateAggregationIntervalInSecForCF; long cfReevaluationCheckInterval; long alarmsReevaluationInterval; TrendzSettings trendzSettings; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 5291ed7a7a..8515e31320 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -195,7 +195,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @Schema(example = "60") private long minAllowedAggregationIntervalInSecForCF = 60; @Schema(example = "300") - private long minAllowedRealtimeAggregationIntervalInSecForCF = 300; + private long minAllowedIntermediateAggregationIntervalInSecForCF = 300; @Schema(example = "60") private long cfReevaluationCheckInterval = 60; @Schema(example = "60") From 8fb6c14f819c2891372f3a11d1e8a9a5de47658b Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 3 Dec 2025 10:02:53 +0200 Subject: [PATCH 04/21] fixed tests --- .../org/thingsboard/server/cf/AlarmRulesTest.java | 12 +++++++----- .../cf/EntityAggregationCalculatedFieldTest.java | 5 +---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index 5f91dab190..0d23465cb4 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbAlarmResult; @@ -86,10 +85,6 @@ import static org.testcontainers.shaded.org.awaitility.Awaitility.await; @Slf4j @DaoSqlTest -@TestPropertySource(properties = { - "actors.calculated_fields.check_interval=1", - "actors.alarms.reevaluation_interval=1" -}) public class AlarmRulesTest extends AbstractControllerTest { @MockitoSpyBean @@ -105,6 +100,13 @@ public class AlarmRulesTest extends AbstractControllerTest { @Before public void beforeEach() throws Exception { + loginSysAdmin(); + + updateDefaultTenantProfileConfig(tenantProfileConfig -> { + tenantProfileConfig.setCfReevaluationCheckInterval(1); + tenantProfileConfig.setAlarmsReevaluationInterval(1); + }); + loginTenantAdmin(); device = createDevice("Device A", "aaa"); deviceId = device.getId(); diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java index 3044525757..4f85129263 100644 --- a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -20,7 +20,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; @@ -54,9 +53,6 @@ import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTE @DaoSqlTest @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -@TestPropertySource(properties = { - "actors.calculated_fields.check_interval=1" -}) public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest { private Tenant savedTenant; @@ -68,6 +64,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest updateDefaultTenantProfileConfig(tenantProfileConfig -> { tenantProfileConfig.setMinAllowedDeduplicationIntervalInSecForCF(1); tenantProfileConfig.setMinAllowedAggregationIntervalInSecForCF(1); + tenantProfileConfig.setCfReevaluationCheckInterval(1); }); Tenant tenant = new Tenant(); From 87e458f29b0a4372b1fc0d7d4d637c8f77dd50e0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 3 Dec 2025 11:18:19 +0200 Subject: [PATCH 05/21] renamed tenant profile property and removed reevaluation interval from system params --- .../src/main/data/upgrade/basic/schema_update.sql | 4 ++-- .../CalculatedFieldManagerMessageProcessor.java | 13 +++++++++---- .../server/controller/SystemInfoController.java | 4 +--- .../server/controller/TenantProfileController.java | 2 +- .../service/cf/DefaultCalculatedFieldCache.java | 2 +- .../service/cf/ctx/state/CalculatedFieldCtx.java | 11 +++-------- .../EntityAggregationCalculatedFieldState.java | 3 +-- .../server/common/data/SystemParams.java | 4 +--- .../profile/DefaultTenantProfileConfiguration.java | 2 +- 9 files changed, 20 insertions(+), 25 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 091da4d4fa..24b9b93eff 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -26,7 +26,7 @@ SET profile_data = jsonb_set( 'maxRelatedEntitiesToReturnPerCfArgument', 100, 'minAllowedDeduplicationIntervalInSecForCF', 10, 'minAllowedAggregationIntervalInSecForCF', 60, - 'minAllowedIntermediateAggregationIntervalInSecForCF', 300, + 'intermediateAggregationIntervalInSecForCF', 300, 'cfReevaluationCheckInterval', 60, 'alarmsReevaluationInterval', 60 ) @@ -40,7 +40,7 @@ WHERE NOT ( 'maxRelatedEntitiesToReturnPerCfArgument', 'minAllowedDeduplicationIntervalInSecForCF', 'minAllowedAggregationIntervalInSecForCF', - 'minAllowedIntermediateAggregationIntervalInSecForCF', + 'intermediateAggregationIntervalInSecForCF', 'cfReevaluationCheckInterval', 'alarmsReevaluationInterval' ] diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index eca0d9447f..7c9a291380 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -116,6 +116,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final TbQueueCalculatedFieldSettings cfSettings; protected final TenantId tenantId; + private long cfCheckInterval; + protected TbActorCtx ctx; CalculatedFieldManagerMessageProcessor(ActorSystemContext systemContext, TenantId tenantId) { @@ -153,6 +155,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId()); initEntitiesCache(); initCalculatedFields(); + cfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); scheduleCfsReevaluation(); msg.getCallback().onSuccess(); } @@ -175,7 +178,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void scheduleCfsReevaluation() { - long cfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { try { calculatedFields.values().forEach(cf -> { @@ -256,12 +258,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) { - cancelReevaluationTask(); - scheduleCfsReevaluation(); + long updatedCfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); + if (cfCheckInterval != updatedCfCheckInterval) { + cancelReevaluationTask(); + scheduleCfsReevaluation(); + } Stream.concat( calculatedFields.values().stream(), entityIdCalculatedFields.values().stream().flatMap(Collection::stream) - ).forEach(CalculatedFieldCtx::updateTenantProfileProperties); + ).forEach(CalculatedFieldCtx::setTenantProfileProperties); callback.onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 581f51d370..126d426415 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -166,9 +166,7 @@ public class SystemInfoController extends BaseController { systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); systemParams.setMinAllowedDeduplicationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedDeduplicationIntervalInSecForCF()); systemParams.setMinAllowedAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedAggregationIntervalInSecForCF()); - systemParams.setMinAllowedIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedIntermediateAggregationIntervalInSecForCF()); - systemParams.setCfReevaluationCheckInterval(tenantProfileConfiguration.getCfReevaluationCheckInterval()); - systemParams.setAlarmsReevaluationInterval(tenantProfileConfiguration.getAlarmsReevaluationInterval()); + systemParams.setIntermediateAggregationIntervalInSecForCF(tenantProfileConfiguration.getIntermediateAggregationIntervalInSecForCF()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index ee6d67209a..f2c345b6d1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -169,7 +169,7 @@ public class TenantProfileController extends BaseController { " \"maxSingleValueArgumentSizeInKBytes\": 2," + " \"minAllowedDeduplicationIntervalInSecForCF\": 10," + " \"minAllowedAggregationIntervalInSecForCF\": 60," + - " \"minAllowedIntermediateAggregationIntervalInSecForCF\": 300," + + " \"intermediateAggregationIntervalInSecForCF\": 300," + " \"cfReevaluationCheckInterval\": 60," + " \"alarmsReevaluationInterval\": 60" + " }\n" + diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index e7c4801c12..9d6727a7fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -239,7 +239,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { TenantProfile tenantProfile = tenantProfileCache.get(ctx.getTenantId()); return tenantProfile != null && tenantProfileId.equals(tenantProfile.getId()); }) - .forEach(CalculatedFieldCtx::updateTenantProfileProperties); + .forEach(CalculatedFieldCtx::setTenantProfileProperties); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 667f52e9c4..020c19c4b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -212,12 +212,7 @@ public class CalculatedFieldCtx implements Closeable { this.alarmService = systemContext.getAlarmService(); this.cfProcessingService = systemContext.getCalculatedFieldProcessingService(); - ApiLimitService apiLimitService = systemContext.getApiLimitService(); - this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; - this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; - this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedIntermediateAggregationIntervalInSecForCF)); - this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); - this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); + setTenantProfileProperties(); } public boolean requiresScheduledReevaluation() { @@ -302,11 +297,11 @@ public class CalculatedFieldCtx implements Closeable { } } - public void updateTenantProfileProperties() { + public void setTenantProfileProperties() { ApiLimitService apiLimitService = systemContext.getApiLimitService(); this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; - this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedIntermediateAggregationIntervalInSecForCF)); + this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getIntermediateAggregationIntervalInSecForCF)); this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index c39aa46235..cd5d32ec7a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -114,7 +114,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt Map> results = new HashMap<>(); List expiredIntervals = new ArrayList<>(); getIntervals().forEach((intervalEntry, argIntervalStatuses) -> { - processInterval(now, ctx, intervalEntry, argIntervalStatuses, expiredIntervals, results); + processInterval(now, intervalEntry, argIntervalStatuses, expiredIntervals, results); }); removeExpiredIntervals(expiredIntervals); @@ -194,7 +194,6 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt } private void processInterval(long now, - CalculatedFieldCtx ctx, AggIntervalEntry intervalEntry, Map args, List expiredIntervals, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 52fa1760de..40bfa668d2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -42,8 +42,6 @@ public class SystemParams { int maxRelationLevelPerCfArgument; long minAllowedDeduplicationIntervalInSecForCF; long minAllowedAggregationIntervalInSecForCF; - long minAllowedIntermediateAggregationIntervalInSecForCF; - long cfReevaluationCheckInterval; - long alarmsReevaluationInterval; + long intermediateAggregationIntervalInSecForCF; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 8515e31320..c633cc0cd5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -195,7 +195,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @Schema(example = "60") private long minAllowedAggregationIntervalInSecForCF = 60; @Schema(example = "300") - private long minAllowedIntermediateAggregationIntervalInSecForCF = 300; + private long intermediateAggregationIntervalInSecForCF = 300; @Schema(example = "60") private long cfReevaluationCheckInterval = 60; @Schema(example = "60") From 9ac61280c51c93e6875fcf44a3ff6f79c0822f58 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 3 Dec 2025 11:46:07 +0200 Subject: [PATCH 06/21] return default value for cf interval if 0 --- .../tenant/profile/DefaultTenantProfileConfiguration.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index c633cc0cd5..915d97398d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -196,6 +196,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long minAllowedAggregationIntervalInSecForCF = 60; @Schema(example = "300") private long intermediateAggregationIntervalInSecForCF = 300; + @Builder.Default @Schema(example = "60") private long cfReevaluationCheckInterval = 60; @Schema(example = "60") @@ -255,4 +256,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura return maxRuleNodeExecutionsPerMessage; } + public long getCfReevaluationCheckInterval() { + return cfReevaluationCheckInterval <= 0 ? 60 : cfReevaluationCheckInterval; + } + } From e4c3ad9bc2d189148654239230b641750ae8b19c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 4 Dec 2025 09:21:43 +0200 Subject: [PATCH 07/21] fixed tests --- ...alculatedFieldManagerMessageProcessor.java | 1 + .../EntityAggregationCalculatedFieldTest.java | 31 +++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 7c9a291380..4f608d7186 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -260,6 +260,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void onTenantProfileUpdated(ComponentLifecycleMsg msg, TbCallback callback) { long updatedCfCheckInterval = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); if (cfCheckInterval != updatedCfCheckInterval) { + cfCheckInterval = updatedCfCheckInterval; cancelReevaluationTask(); scheduleCfsReevaluation(); } diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java index 4f85129263..253d02ebe5 100644 --- a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -94,9 +94,8 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); - long intervalEndTs = customInterval.getCurrentIntervalEndTs(); + createConsumptionCF(device.getId(), customInterval, null); - CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, null); long interval = customInterval.getCurrentIntervalDurationMillis(); await().alias("create CF and no telemetry during interval -> save metric with default value") @@ -115,8 +114,9 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + createConsumptionCF(device.getId(), customInterval, null); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); - long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long tsBeforeInterval = currentIntervalStartTs - 1000; long tsInInterval_1 = currentIntervalStartTs + 1000; @@ -128,7 +128,6 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); long interval = customInterval.getCurrentIntervalDurationMillis(); - CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, null); await().alias("create CF -> perform aggregation after interval end") .atMost(2 * interval, TimeUnit.MILLISECONDS) @@ -158,8 +157,10 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + Watermark watermark = new Watermark(10); + createConsumptionCF(device.getId(), customInterval, watermark); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); - long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long tsBeforeInterval = currentIntervalStartTs - 1000L; long tsInInterval_1 = currentIntervalStartTs + 1000L; @@ -171,8 +172,6 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); long interval = customInterval.getCurrentIntervalDurationMillis(); - Watermark watermark = new Watermark(10); - CalculatedField consumptionCF = createConsumptionCF(device.getId(), customInterval, watermark); await().alias("create CF -> perform aggregation after interval end") .atMost(2 * interval, TimeUnit.MILLISECONDS) @@ -232,27 +231,21 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest Device device = createDevice("Device", "1234567890111"); CustomInterval customInterval = new CustomInterval("Europe/Kyiv", 0L, 5L); + createCFWith2Args(device.getId(), customInterval, null); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); - long currentIntervalEndTs = customInterval.getCurrentIntervalEndTs(); long tsBeforeInterval = currentIntervalStartTs - 1000; long tsInInterval_1 = currentIntervalStartTs + 1000; long tsInInterval_2 = currentIntervalStartTs + 500; long tsInInterval_3 = currentIntervalStartTs + 200; - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsBeforeInterval)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100}}", tsInInterval_1)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); - - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":43}}", tsBeforeInterval)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":39}}", tsInInterval_1)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":27}}", tsInInterval_2)); - postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"temperature\":50}}", tsInInterval_3)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120, \"temperature\":43}}", tsBeforeInterval)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":100, \"temperature\":39}}", tsInInterval_1)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180, \"temperature\":27}}", tsInInterval_2)); + postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120, \"temperature\":50}}", tsInInterval_3)); long interval = customInterval.getCurrentIntervalDurationMillis(); - CalculatedField consumptionCF = createCFWith2Args(device.getId(), customInterval, null); - await().alias("create CF -> perform aggregation after interval end") .atMost(2 * interval, TimeUnit.MILLISECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) From 3704e75838c3145dcd7eb122699457c93a564a92 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 4 Dec 2025 12:59:27 +0200 Subject: [PATCH 08/21] UI: Fix CVE-2025-66035 --- ui-ngx/patches/@angular+common+18.2.13.patch | 66 ++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 ui-ngx/patches/@angular+common+18.2.13.patch diff --git a/ui-ngx/patches/@angular+common+18.2.13.patch b/ui-ngx/patches/@angular+common+18.2.13.patch new file mode 100644 index 0000000000..984562138a --- /dev/null +++ b/ui-ngx/patches/@angular+common+18.2.13.patch @@ -0,0 +1,66 @@ +diff --git a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs +index da69c17..d17f6ad 100755 +--- a/node_modules/@angular/common/esm2022/http/src/xsrf.mjs ++++ b/node_modules/@angular/common/esm2022/http/src/xsrf.mjs +@@ -19,6 +19,10 @@ export const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME + providedIn: 'root', + factory: () => XSRF_DEFAULT_HEADER_NAME, + }); ++/** ++ * Regex to match absolute URLs, including protocol-relative URLs. ++ */ ++const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i; + /** + * Retrieves the current XSRF token to use with the next outgoing request. + * +@@ -69,7 +73,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo + args: [XSRF_COOKIE_NAME] + }] }] }); + export function xsrfInterceptorFn(req, next) { +- const lcUrl = req.url.toLowerCase(); + // Skip both non-mutating requests and absolute URLs. + // Non-mutating requests don't require a token, and absolute URLs require special handling + // anyway as the cookie set +@@ -77,8 +80,7 @@ export function xsrfInterceptorFn(req, next) { + if (!inject(XSRF_ENABLED) || + req.method === 'GET' || + req.method === 'HEAD' || +- lcUrl.startsWith('http://') || +- lcUrl.startsWith('https://')) { ++ ABSOLUTE_URL_REGEX.test(req.url)) { + return next(req); + } + const token = inject(HttpXsrfTokenExtractor).getToken(); +diff --git a/node_modules/@angular/common/fesm2022/http.mjs b/node_modules/@angular/common/fesm2022/http.mjs +index 1655480..d1dbb38 100755 +--- a/node_modules/@angular/common/fesm2022/http.mjs ++++ b/node_modules/@angular/common/fesm2022/http.mjs +@@ -2352,6 +2352,10 @@ const XSRF_HEADER_NAME = new InjectionToken(ngDevMode ? 'XSRF_HEADER_NAME' : '', + providedIn: 'root', + factory: () => XSRF_DEFAULT_HEADER_NAME, + }); ++/** ++ * Regex to match absolute URLs, including protocol-relative URLs. ++ */ ++const ABSOLUTE_URL_REGEX = /^(?:https?:)?\/\//i; + /** + * Retrieves the current XSRF token to use with the next outgoing request. + * +@@ -2402,7 +2406,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo + args: [XSRF_COOKIE_NAME] + }] }] }); + function xsrfInterceptorFn(req, next) { +- const lcUrl = req.url.toLowerCase(); + // Skip both non-mutating requests and absolute URLs. + // Non-mutating requests don't require a token, and absolute URLs require special handling + // anyway as the cookie set +@@ -2410,8 +2413,7 @@ function xsrfInterceptorFn(req, next) { + if (!inject(XSRF_ENABLED) || + req.method === 'GET' || + req.method === 'HEAD' || +- lcUrl.startsWith('http://') || +- lcUrl.startsWith('https://')) { ++ ABSOLUTE_URL_REGEX.test(req.url)) { + return next(req); + } + const token = inject(HttpXsrfTokenExtractor).getToken(); From 2bc8ec0215cd93606a2334d15c27970d40ac0903 Mon Sep 17 00:00:00 2001 From: samuel Date: Thu, 4 Dec 2025 15:35:18 +0800 Subject: [PATCH 09/21] fix: Error 'TypeError: Missing parameter name ...' when running TB_ENABLE_PROXY=true --- msa/web-ui/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/msa/web-ui/server.ts b/msa/web-ui/server.ts index bee2e5af31..db0a464499 100644 --- a/msa/web-ui/server.ts +++ b/msa/web-ui/server.ts @@ -81,12 +81,12 @@ let connections: Socket[] = []; } } }); - app.all('/api/*', (req, res) => { + app.all('/api/*splat', (req, res) => { logger.debug(req.method + ' ' + req.originalUrl); apiProxy.web(req, res); }); - app.all('/static/rulenode/*', (req, res) => { + app.all('/static/rulenode/*splat', (req, res) => { apiProxy.web(req, res); }); From f92e21238300ea8204d59f497b6699de67047320 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 4 Dec 2025 16:13:56 +0200 Subject: [PATCH 10/21] UI: Fix CVE-2025-66412 --- .../patches/@angular+compiler+18.2.13.patch | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 ui-ngx/patches/@angular+compiler+18.2.13.patch diff --git a/ui-ngx/patches/@angular+compiler+18.2.13.patch b/ui-ngx/patches/@angular+compiler+18.2.13.patch new file mode 100644 index 0000000000..a4245cb0e9 --- /dev/null +++ b/ui-ngx/patches/@angular+compiler+18.2.13.patch @@ -0,0 +1,88 @@ +diff --git a/node_modules/@angular/compiler/fesm2022/compiler.mjs b/node_modules/@angular/compiler/fesm2022/compiler.mjs +index a00b189..260e7be 100755 +--- a/node_modules/@angular/compiler/fesm2022/compiler.mjs ++++ b/node_modules/@angular/compiler/fesm2022/compiler.mjs +@@ -18631,6 +18631,7 @@ function SECURITY_SCHEMA() { + 'area|ping', + 'audio|src', + 'a|href', ++ 'a|xlink:href', + 'a|ping', + 'blockquote|cite', + 'body|background', +@@ -18644,6 +18645,75 @@ function SECURITY_SCHEMA() { + 'track|src', + 'video|poster', + 'video|src', ++ ++ // MathML namespace ++ // https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1 ++ 'annotation|href', ++ 'annotation|xlink:href', ++ 'annotation-xml|href', ++ 'annotation-xml|xlink:href', ++ 'maction|href', ++ 'maction|xlink:href', ++ 'malignmark|href', ++ 'malignmark|xlink:href', ++ 'math|href', ++ 'math|xlink:href', ++ 'mroot|href', ++ 'mroot|xlink:href', ++ 'msqrt|href', ++ 'msqrt|xlink:href', ++ 'merror|href', ++ 'merror|xlink:href', ++ 'mfrac|href', ++ 'mfrac|xlink:href', ++ 'mglyph|href', ++ 'mglyph|xlink:href', ++ 'msub|href', ++ 'msub|xlink:href', ++ 'msup|href', ++ 'msup|xlink:href', ++ 'msubsup|href', ++ 'msubsup|xlink:href', ++ 'mmultiscripts|href', ++ 'mmultiscripts|xlink:href', ++ 'mprescripts|href', ++ 'mprescripts|xlink:href', ++ 'mi|href', ++ 'mi|xlink:href', ++ 'mn|href', ++ 'mn|xlink:href', ++ 'mo|href', ++ 'mo|xlink:href', ++ 'mpadded|href', ++ 'mpadded|xlink:href', ++ 'mphantom|href', ++ 'mphantom|xlink:href', ++ 'mrow|href', ++ 'mrow|xlink:href', ++ 'ms|href', ++ 'ms|xlink:href', ++ 'mspace|href', ++ 'mspace|xlink:href', ++ 'mstyle|href', ++ 'mstyle|xlink:href', ++ 'mtable|href', ++ 'mtable|xlink:href', ++ 'mtd|href', ++ 'mtd|xlink:href', ++ 'mtr|href', ++ 'mtr|xlink:href', ++ 'mtext|href', ++ 'mtext|xlink:href', ++ 'mover|href', ++ 'mover|xlink:href', ++ 'munder|href', ++ 'munder|xlink:href', ++ 'munderover|href', ++ 'munderover|xlink:href', ++ 'semantics|href', ++ 'semantics|xlink:href', ++ 'none|href', ++ 'none|xlink:href', + ]); + registerContext(SecurityContext.RESOURCE_URL, [ + 'applet|code', From 271ac0120ece3620ced3605ad3cd7bc562c6defd Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 5 Dec 2025 12:22:17 +0200 Subject: [PATCH 11/21] UI: Bug-fix and enh for cf --- .../alarm-rule-dialog.component.html | 60 +++++---- .../alarm-rule-dialog.component.ts | 62 +++++++++- .../alarm-rules/alarm-rules-table-config.ts | 2 +- ...alarm-rule-condition-dialog.component.html | 4 +- ...f-alarm-rule-condition-dialog.component.ts | 45 ++----- .../cf-alarm-rule-condition.component.html | 9 +- .../cf-alarm-rule-condition.component.ts | 30 ++--- .../alarm-rules/cf-alarm-rule.component.html | 50 ++++---- .../create-cf-alarm-rules.component.html | 14 +-- .../alarm-rule-filter-dialog.component.ts | 33 +---- .../alarm-rule-filter-list.component.html | 115 +++++++++--------- .../alarm-rule-filter-list.component.ts | 42 ++----- ...-rule-filter-predicate-list.component.html | 92 +++++++------- ...ilter-predicate-no-data-value.component.ts | 2 +- ...m-rule-filter-predicate-value.component.ts | 2 +- ...alarm-rule-filter-predicate.component.html | 27 ++-- .../alarm-rule-filter-predicate.component.ts | 60 +++------ .../alarm-rule-filter-text.component.html | 1 + .../alarm-rule-filter-text.component.scss | 3 + .../alarm-rule-filter-text.component.ts | 4 + ...lated-field-arguments-table.component.html | 18 +-- ...culated-field-arguments-table.component.ts | 5 +- .../calculated-field-dialog.component.html | 2 +- ...eofencing-zone-groups-table.component.html | 12 +- ...-geofencing-zone-groups-table.component.ts | 2 - ...culated-field-metrics-table.component.html | 15 ++- ...ities-aggregation-component.component.html | 16 +-- .../simple-configuration.component.html | 1 + .../relation/relation-table.component.html | 16 ++- .../relation/relation-table.component.scss | 11 ++ .../entity/entity-autocomplete.component.ts | 4 + .../entity/entity-type-select.component.ts | 4 + .../import-export/import-export.service.ts | 4 +- .../app/shared/models/alarm-rule.models.ts | 70 +++++++++-- .../alarm-rule/alarm_rule_schedule_format.md | 2 +- .../assets/locale/locale.constant-en_US.json | 30 ++--- 36 files changed, 450 insertions(+), 419 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index 20bd3e1bde..343c3123ab 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -29,10 +29,11 @@
{{ 'common.general' | translate }}
-
+
{{ 'alarm-rule.alarm-type' | translate }} + alarm-rule.alarm-type-hint @if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) { @if (fieldFormGroup.get('name').hasError('required')) { @@ -48,15 +49,18 @@
@if (!data.entityId) { -
- + @if (fieldFormGroup.get('entityId.entityType').value) { - }
@@ -85,8 +90,8 @@ [tenantId]="data.tenantId" [ownerId]="data.ownerId" [watchKeyChange]="true" - [disabledAddButton]="!fieldFormGroup.get('entityId.id').value" - [entityName]="data.entityName"/> + [disable]="!fieldFormGroup.get('entityId.id').value || !fieldFormGroup.get('name').value" + [entityName]="entityName"/>
{{ 'alarm-rule.create-conditions' | translate }}
@@ -113,11 +118,11 @@
- alarm-rule.no-clear-alarm-rule + alarm-rule.no-clear-alarm-rule
@if (configFormGroup.get('propagate').value) { - - alarm-rule.alarm-rule-relation-types-list - - - {{key}} - close - - - - - + + }
@@ -181,7 +177,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index f45e205246..fea0df6024 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -40,6 +40,11 @@ import { import { deepTrim } from "@core/utils"; import { Observable } from "rxjs"; import { switchMap } from "rxjs/operators"; +import { EntityTypeSelectComponent } from "@shared/components/entity/entity-type-select.component"; +import { EntityAutocompleteComponent } from "@shared/components/entity/entity-autocomplete.component"; +import { EntityService } from "@core/http/entity.service"; +import { RelationTypes } from "@shared/models/relation.models"; +import { StringItemsOption } from "@shared/components/string-items-list.component"; export interface AlarmRuleDialogData { value?: CalculatedField; @@ -66,7 +71,7 @@ export class AlarmRuleDialogComponent extends DialogComponent(null, Validators.required), + entityType: this.fb.control(EntityType.DEVICE_PROFILE, Validators.required), id: [null as null | string, Validators.required], }), configuration: this.fb.group({ @@ -93,15 +98,45 @@ export class AlarmRuleDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, protected dialogRef: MatDialogRef, private calculatedFieldsService: CalculatedFieldsService, + private entityService: EntityService, private destroyRef: DestroyRef, private fb: FormBuilder) { super(store, router, dialogRef); this.applyDialogData(); + this.updateRulesValidators(); + + this.fieldFormGroup.get('configuration.arguments').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateRulesValidators(); + }); + + if (!this.entityName) { + this.fieldFormGroup.get('entityId.id').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((entityId) => { + if (entityId && (this.fieldFormGroup.get('entityId.entityType').value === EntityType.DEVICE_PROFILE || + this.fieldFormGroup.get('entityId.entityType').value === EntityType.ASSET_PROFILE)) { + this.entityService.getEntity(this.fieldFormGroup.get('entityId.entityType').value as EntityType, entityId, {ignoreLoading: true, ignoreErrors: true}).subscribe( + value => { + this.entityName = value.name; + } + ) + } + }); + } } get configFormGroup(): FormGroup { @@ -169,6 +204,10 @@ export class AlarmRuleDialogComponent extends DialogComponent this.dialogRef.close(calculatedField)); + } else { + this.fieldFormGroup.get('name').markAsTouched(); + this.entityTypeSelect.markAsTouched(); + this.entityAutocompleteComponent.markAsTouched(); } } @@ -191,4 +230,23 @@ export class AlarmRuleDialogComponent extends DialogComponent 0) { + this.fieldFormGroup.get('configuration.createRules').enable({emitEvent: false}); + this.fieldFormGroup.get('configuration.clearRule').enable({emitEvent: false}); + this.disabledClearRuleButton = true; + } else { + this.fieldFormGroup.get('configuration.createRules').disable({emitEvent: false}); + this.fieldFormGroup.get('configuration.clearRule').disable({emitEvent: false}); + this.disabledClearRuleButton = false; + } + } + get predefinedTypeValues(): StringItemsOption[] { + return RelationTypes.map(type => ({ + name: type, + value: type + })); + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index b20f1cee7b..f73633ad8e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -287,7 +287,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private importCalculatedField(): void { - this.importExportService.openCalculatedFieldImportDialog() + this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file') .pipe( filter(Boolean), switchMap(calculatedField => { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index d67cd51a9a..e7bc32a19f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -75,14 +75,14 @@ matTooltipPosition="above" class="tb-mat-32" [disabled]="!argumentsList.length" - (click)="onTestScript()"> + (click)="onTestScript($event)"> bug_report
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts index f565a66168..20ad4a4bfb 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts @@ -41,6 +41,7 @@ import { alarmRuleDefaultScript, AlarmRuleExpressionType, AlarmRuleFilter, + areFiltersAndPredicateArgumentsValid, filterOperationTranslationMap } from "@shared/models/alarm-rule.models"; @@ -137,7 +138,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { - this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList); + this.filtersValid = areFiltersAndPredicateArgumentsValid(filters, this.data.arguments); this.checkIsNoData(filters); }); @@ -206,39 +207,6 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { this.conditionFormGroup.get('expression.expression').setValue(expression); diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html index 638075849f..4892fcdecd 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html @@ -26,13 +26,15 @@
- + {{ conditionSet() ? 'edit' : 'add' }}
@@ -48,7 +50,10 @@ (click)="openScheduleDialog($event)">
- edit + + edit +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 6f17aa8b59..08004d7497 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -32,7 +32,7 @@ import { getAlarmScheduleRangeText, utcTimestampToTimeOfDay } from '@shared/models/device.models'; -import { TimeUnit } from '@shared/models/time/time.models'; +import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { CfAlarmRuleConditionDialogComponent, CfAlarmRuleConditionDialogData @@ -42,7 +42,8 @@ import { AlarmRuleConditionType, AlarmRuleExpressionType, AlarmRuleSchedule, - AlarmRuleScheduleType + AlarmRuleScheduleType, + checkPredicates } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { @@ -154,32 +155,17 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali return !arg || validArguments.includes(arg); } - private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean { - const validSet = new Set(validArguments); + private areFilterAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); const filters = obj?.expression?.filters || obj?.filters || []; for (const filter of filters) { if (filter.argument && !validSet.has(filter.argument)) { return false; } } - function checkPredicates(predicates: any[]): boolean { - for (const p of predicates) { - if (p.value?.dynamicValueArgument) { - if (!validSet.has(p.value.dynamicValueArgument)) { - return false; - } - } - if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) { - if (!checkPredicates(p.predicates)) { - return false; - } - } - } - return true; - } for (const filter of filters) { if (Array.isArray(filter.predicates)) { - if (!checkPredicates(filter.predicates)) { + if (!checkPredicates(filter.predicates, validSet)) { return false; } } @@ -192,7 +178,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali } public validate(control: AbstractControl): ValidationErrors | null { - this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments)); + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); this.onValidatorChange = () => { control.updateValueAndValidity({ emitEvent: true }); @@ -265,7 +251,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (this.modelValue.value.dynamicValueArgument) { this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { attribute: `${this.modelValue.value.dynamicValueArgument}` - }); + }) + ' ' + this.translate.instant(timeUnitTranslationMap.get(this.modelValue.unit)).toLowerCase(); } else { this.specText = this.translate.instant('alarm-rule.condition-during', { during: duringText diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index a9f4ad61a5..a7ccc25fe6 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -18,33 +18,29 @@
- @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { -
-
- alarm-rule.alarm-rule-additional-info -
- - - - +
+
+ alarm-rule.alarm-rule-additional-info
- } - @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { -
-
- alarm-rule.alarm-rule-mobile-dashboard -
- - + + + + +
+
+
+ alarm-rule.alarm-rule-mobile-dashboard
- } + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html index 359ad104f1..bde332b383 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -16,7 +16,7 @@ --> -
+
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
} -
- + @if (!createAlarmRulesFormArray().controls.length) { + alarm-rule.add-create-alarm-rule-prompt - -
-
+ + } +
+
- } -
-
-
-
{{ filterControl.value?.argument }}
-
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
- -
+ @if (index) { + + }
- @if (index) { - - } -
- } - @if (!filtersFormArray.length) { - - alarm-rule.no-filter - - - } -
- + } +
+ +} @else { + alarm-rule.no-filter +} + +
+ @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { +
+ @if (index) { +
+ {{ complexOperationTranslations.get(operation) | translate }} +
+ } +
+
+ + + +
-
- } - @if (!predicatesFormArray.length) { - - alarm-rule.no-filter - - - } + } +
-
-
+ } @else { + alarm-rule.no-filter + } +
} } - @if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) { - - - } @else if (type !== filterPredicateType.COMPLEX) { - - - } + + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts index 2f542f048e..32791ade96 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts @@ -34,6 +34,7 @@ import { alarmRuleNumericOperationTranslationMap, AlarmRuleStringOperation, alarmRuleStringOperationTranslationMap, + checkPredicates, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; import { MatDialog } from "@angular/material/dialog"; @@ -111,6 +112,18 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, this.updateModel(); }); + this.filterPredicateFormGroup.get('operation').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(value => { + if (value === 'NO_DATA') { + this.filterPredicateFormGroup.get('duration').enable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').disable({emitEvent: false}); + } else { + this.filterPredicateFormGroup.get('duration').disable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').enable({emitEvent: false}); + } + }) + this.filterPredicateFormGroup.get('predicates').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(predicates => { @@ -140,25 +153,10 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, } } - private isPredicateArgumentsValid(predicates: any): boolean { + private isPredicateArgumentsValid(predicates: AlarmRuleFilterPredicate[]): boolean { const validSet = new Set(Object.keys(this.arguments)); - function checkPredicates(predicates: any[]): boolean { - for (const p of predicates) { - if (p.value?.dynamicValueArgument) { - if (!validSet.has(p.value.dynamicValueArgument)) { - return false; - } - } - if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) { - if (!checkPredicates(p.predicates)) { - return false; - } - } - } - return true; - } if (Array.isArray(predicates)) { - if (!checkPredicates(predicates)) { + if (!checkPredicates(predicates, validSet)) { return false; } } @@ -172,8 +170,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, } if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) { this.type = AlarmRuleFilterPredicateType[this.valueType]; + this.filterPredicateFormGroup.get('duration').enable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').disable({emitEvent: false}); this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false}); } else { + this.filterPredicateFormGroup.get('duration').disable({emitEvent: false}); + this.filterPredicateFormGroup.get('value').enable({emitEvent: false}); this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); } } @@ -183,30 +185,6 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, if (predicate.operation === 'NO_DATA') { this.propagateChange(predicate.duration); } else { - if (!predicate.value) { - switch (this.valueType) { - case EntityKeyValueType.STRING: - predicate.value = { - staticValue: '' - }; - break; - case EntityKeyValueType.NUMERIC: - predicate.value = { - staticValue: 0 - }; - break; - case EntityKeyValueType.DATE_TIME: - predicate.value = { - staticValue: Date.now() - }; - break; - case EntityKeyValueType.BOOLEAN: - predicate.value = { - staticValue: false - }; - break; - } - } this.propagateChange({type: this.type, ...predicate}); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html index 4ecba0f3d2..d070c2de84 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html @@ -18,6 +18,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss index 8712fdfd32..dbaf7dd966 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss @@ -23,6 +23,9 @@ color: #f44336; padding: 0; } + &.disabled { + color: rgba(0,0,0,0.38); + } &.nowrap { white-space: nowrap; text-overflow: ellipsis; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts index 703c41665e..aa8aa2bfec 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts @@ -63,6 +63,10 @@ export class AlarmRuleFilterTextComponent { @Input() arguments: Record; + @Input() + @coerceBoolean() + disabled = false; + private alarmRuleExpressionValue: AlarmRuleExpression; get alarmRuleExpression(): AlarmRuleExpression { return this.alarmRuleExpressionValue; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index 6b8f4fda79..d887c0f40a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -15,8 +15,10 @@ limitations under the License. --> -
-
+
+
@@ -94,6 +96,7 @@
-
- {{ 'calculated-fields.no-arguments' | translate }} -
@if (errorText || (dataSource.isEmpty() | async)) { }
+
+ {{ 'calculated-fields.no-arguments' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index bca335211e..002a515164 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + booleanAttribute, ChangeDetectorRef, Component, DestroyRef, @@ -87,7 +88,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces @Input() entityName: string; @Input() ownerId: EntityId; @Input() isScript: boolean; - @Input() disabledAddButton = false; + @Input({transform: booleanAttribute}) disable = false; @Input() watchKeyChange = false; @ViewChild(MatSort, { static: true }) sort: MatSort; @@ -220,8 +221,6 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; } else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; - } else if (!this.argumentsFormArray.controls.length) { - this.errorText = 'calculated-fields.hint.arguments-empty'; } else { this.errorText = ''; } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 3070a6cbb2..14ec6d1ce8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ 'entity.type-calculated-field' | translate}}

diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index dcfd37796d..67ec7f097b 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -16,7 +16,9 @@ -->
-
+
@@ -121,14 +123,14 @@ *matHeaderRowDef="['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']">
-
- {{ 'calculated-fields.no-zone-configured' | translate }} -
@if (errorText) { }
+
+ {{ 'calculated-fields.no-zone-configured' | translate }} +
-
- -
-
calculated-fields.use-latest-timestamp
-
-
-
+ @if (relatedAggregationConfiguration.get('output').value?.type === OutputType.Timeseries) { +
+ +
+
calculated-fields.use-latest-timestamp
+
+
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html index cfab9d9def..178f8b47d6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -23,6 +23,7 @@ [tenantId]="tenantId" [ownerId]="ownerId" [entityName]="entityName" + [watchKeyChange]="true" [isScript]="isScript" />
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index e836e5e28b..67f4f86970 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -104,8 +104,20 @@ {{ 'relation.type' | translate }} - - {{ relation.type }} + +
+ {{ relation.type }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss index 263ebfeed3..a7dffdfe0a 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -59,6 +59,17 @@ overflow: hidden; text-overflow: ellipsis; } + + .type-copy { + visibility: hidden; + transition: visibility 0.1s; + } + + .type:hover { + .type-copy { + visibility: visible; + } + } } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index be8c1cd4c5..dba5351df3 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -475,4 +475,8 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit get showEntityLink(): boolean { return this.selectEntityFormGroup.get('entity').value && this.disabled && this.entityURL !== ''; } + + markAsTouched(): void { + this.selectEntityFormGroup.get('entity').markAsTouched(); + } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts index 9e9ffcb474..fcfe158da0 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts @@ -173,4 +173,8 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, return ''; } } + + markAsTouched(): void { + this.entityTypeFormGroup.get('entityType').markAsTouched(); + } } diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 34e584a527..d416917bb8 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -183,8 +183,8 @@ export class ImportExportService { }); } - public openCalculatedFieldImportDialog(): Observable { - return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe( + public openCalculatedFieldImportDialog(importTitle = 'calculated-fields.import', importFileLabel = 'calculated-fields.file'): Observable { + return this.openImportDialog(importTitle, importFileLabel).pipe( catchError(() => of(null)), ); } diff --git a/ui-ngx/src/app/shared/models/alarm-rule.models.ts b/ui-ngx/src/app/shared/models/alarm-rule.models.ts index 484c6be3a2..3d94461684 100644 --- a/ui-ngx/src/app/shared/models/alarm-rule.models.ts +++ b/ui-ngx/src/app/shared/models/alarm-rule.models.ts @@ -18,17 +18,10 @@ import { CustomTimeSchedulerItem } from "@shared/models/device.models"; import { DashboardId } from "@shared/models/id/dashboard-id"; import { TimeUnit } from "@shared/models/time/time.models"; -import { - BooleanOperation, - ComplexOperation, - EntityKeyValueType, - FilterPredicateType, - NumericOperation, - StringOperation -} from "@shared/models/query/query.models"; +import { ComplexOperation, EntityKeyValueType, FilterPredicateType } from "@shared/models/query/query.models"; import { EntityType } from "@shared/models/entity-type.models"; import { Observable } from "rxjs"; -import { CalculatedField } from "@shared/models/calculated-field.models"; +import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models"; export enum AlarmRuleScheduleType { ANY_TIME = 'ANY_TIME', @@ -250,3 +243,62 @@ export const alarmRuleDefaultScript = 'return temperature > 20;' export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, expression: string, argumentsObj?: Record, closeAllOnSave?: boolean) => Observable; + +export function checkPredicates(predicates: any[], validSet: Set): boolean { + for (const predicate of predicates) { + if (!predicate) continue; + if (predicate?.value?.dynamicValueArgument) { + if (!validSet.has(predicate.value.dynamicValueArgument)) { + return false; + } + } + if (predicate.type === 'COMPLEX' && Array.isArray(predicate.predicates)) { + if (!checkPredicates(predicate.predicates, validSet)) { + return false; + } + } + } + return true; +} + +export function areFilterAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); + const filter = obj || []; + if (filter.argument && !validSet.has(filter.argument)) { + return false; + } + if (Array.isArray(filter.predicates)) { + if (!checkPredicates(filter.predicates, validSet)) { + return false; + } + } + return true; +} + +export function areFiltersAndPredicateArgumentsValid(obj: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); + const filters = obj || []; + for (const filter of filters) { + if (filter.argument && !validSet.has(filter.argument)) { + return false; + } + } + for (const filter of filters) { + if (Array.isArray(filter.predicates)) { + if (!checkPredicates(filter.predicates, validSet)) { + return false; + } + } + } + return true; +} + +export function isPredicateArgumentsValid(predicates: any, args: Record): boolean { + const validSet = new Set(Object.keys(args)); + if (Array.isArray(predicates)) { + if (!checkPredicates(predicates, validSet)) { + return false; + } + } + return true; +} diff --git a/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md index c49d1029bf..bc332f1ea5 100644 --- a/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md +++ b/ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md @@ -49,7 +49,7 @@ The argument value for a specific time schedule must be a JSON object in the fol ```javascript { - "type": "CUSTOM" + "type": "CUSTOM", "timezone": "Europe/Kiev", "items": [ { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 477c1fa012..dc11b649e7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1122,7 +1122,7 @@ "add-argument": "Add argument", "test-script-function": "Test script function", "test-expression-function": "Test expression function", - "no-arguments": "No arguments configured", + "no-arguments": "At least one argument is required.", "argument-settings": "Argument settings", "argument-current": "Current entity", "argument-current-tenant": "Current tenant", @@ -1176,7 +1176,7 @@ "target-zone": "Target zone", "perimeter-key": "Perimeter key", "report-strategy": "Report strategy", - "no-zone-configured": "No zone group configured", + "no-zone-configured": "At least one zone is required.", "no-zone-configured-required": "At least one zone group must be configured.", "add-zone-group": "Add zone group", "report-transition-event-only": "Transition events only", @@ -1245,7 +1245,7 @@ "key": "Key", "function": "Function" }, - "no-metrics-configured": "No metrics configured", + "no-metrics-configured": "At least one metric is required.", "add-metric": "Add metric", "max-metrics": "Maximum number of metrics reached.", "metric-settings": "Metric settings", @@ -1268,7 +1268,7 @@ "ttl-min": "Only 0 minimum TTL is allowed", "processing-parameters": "Processing parameters", "hint": { - "strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing", + "strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing.", "processing-options": "Processing options", "update-attribute-only-on-value-change": "Updates attribute on every incoming message, regardless of whether the value has changed. This increases API usage and reduces performance.", "update-attribute-only-on-value-change-enabled": "Updates attribute only when the value changes. If the value is unchanged, timestamps are not updated and notifications are not sent.", @@ -1380,7 +1380,7 @@ "alarm-rules-old": "Old", "alarm-rules-actual": "Actual", "severities": "Severities", - "cleared": "Clearing condition", + "cleared": "Clear condition", "delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?", "delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.", "delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?", @@ -1392,6 +1392,7 @@ "list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }", "selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected", "import": "Import alarm rule", + "file": "Alarm rule file", "export": "Export alarm rule", "export-failed-error": "Unable to export alarm rule: {{error}}", "entity-type": "Entity type", @@ -1400,6 +1401,7 @@ "target-entity": "Target entity", "target-entities": "Target entities", "alarm-type": "Alarm type", + "alarm-type-hint": "Unique identifier (e.g., HighTemperatureAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.", "alarm-type-required": "Alarm type is required.", "alarm-type-pattern": "Alarm type is invalid.", "alarm-type-max-length": "Alarm type should be less than 256 characters.", @@ -1435,8 +1437,7 @@ "add-filter": "Add argument filter", "edit-filter": "Argument filter", "remove-filter": "Remove argument filter", - "no-filter": "No argument filters configured", - "filter-required": "At least one filter must be configured.", + "no-filter": "At least one filter is required.", "conditions": { "simple": "Simple", "duration": "Duration", @@ -1495,9 +1496,9 @@ "condition-type": "Condition type", "condition-type-hint": "\"Duration\" and \"Repeating\" options are not available when the \"Missing for\" operation is used in the filter.", "select-alarm-severity": "Select alarm severity", - "add-create-alarm-rule-prompt": "At least one creation condition should be configured", - "add-create-alarm-rule": "Add creation condition", - "add-clear-alarm-rule": "Add clearing condition", + "add-create-alarm-rule-prompt": "At least one trigger condition is required.", + "add-create-alarm-rule": "Add trigger condition", + "add-clear-alarm-rule": "Add clear condition", "condition-duration": "Condition duration", "condition-duration-value": "Duration value", "condition-duration-time-unit": "Time unit", @@ -1510,9 +1511,9 @@ "condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.", "condition-repeating-value-pattern": "Count of events should be integers.", "condition-repeating-value-required": "Count of events is required.", - "create-conditions": "Creation conditions", - "clear-condition": "Clearing condition", - "no-clear-alarm-rule": "Clearing condition not configured", + "create-conditions": "Alarm Trigger Conditions", + "clear-condition": "Alarm Clear Condition", + "no-clear-alarm-rule": "No clear condition configured.", "advanced-settings": "Advanced settings", "propagate-alarm": "Propagate alarm to related entities", "alarm-rule-relation-types-list": "Relation types", @@ -4981,7 +4982,8 @@ "additional-info": "Additional info (JSON)", "invalid-additional-info": "Unable to parse additional info json.", "no-relations-text": "No relations found", - "not": "Not" + "not": "Not", + "copy-type": "Copy type" }, "resource": { "add": "Add resource", From 5649a4c626683e8a827abb5f4a2e3fea51d8d1db Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 5 Dec 2025 15:42:36 +0200 Subject: [PATCH 12/21] UI: Add Produce intermediate result to entity aggregation --- ui-ngx/src/app/core/auth/auth.models.ts | 1 + ui-ngx/src/app/core/auth/auth.reducer.ts | 1 + ...ntity-aggregation-component.component.html | 9 ++++ .../entity-aggregation-component.component.ts | 54 +++++++++++++++++-- ...enant-profile-configuration.component.html | 44 +++++++++++++++ ...-tenant-profile-configuration.component.ts | 3 ++ .../shared/models/calculated-field.models.ts | 1 + ui-ngx/src/app/shared/models/tenant.model.ts | 7 +++ .../assets/locale/locale.constant-en_US.json | 13 ++++- 9 files changed, 128 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index 21759fbca0..9218b09df1 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -37,6 +37,7 @@ export interface SysParamsState { maxRelationLevelPerCfArgument: number; ruleChainDebugPerTenantLimitsConfiguration?: string; calculatedFieldDebugPerTenantLimitsConfiguration?: string; + intermediateAggregationIntervalInSecForCF: number; trendzSettings: TrendzSettings; } diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index af040a6d53..aad609356a 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -39,6 +39,7 @@ const emptyUserAuthState: AuthPayload = { maxRelationLevelPerCfArgument: 0, maxDataPointsPerRollingArg: 0, maxDebugModeDurationMinutes: 0, + intermediateAggregationIntervalInSecForCF: 0, userSettings: initialUserSettings, trendzSettings: initialTrendzSettings }; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html index 4dcbf5b38d..c49c6f25cc 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html @@ -124,6 +124,15 @@ }
+
+ +
+ {{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }} +
+
+
Observable; readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF; + readonly intermediateAggregationIntervalInSecForCF = getCurrentAuthState(this.store).intermediateAggregationIntervalInSecForCF; readonly DayInSec = DAY / SECOND; entityAggregationConfiguration = this.fb.group({ @@ -104,6 +105,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor watermark: this.fb.group({ duration: [HOUR/SECOND, Validators.required], }), + produceIntermediateResult: [false], output: this.fb.control(defaultCalculatedFieldOutput), }); @@ -153,6 +155,15 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.updatedOffsetHint(); }); + merge( + this.entityAggregationConfiguration.get('interval.type').valueChanges, + this.entityAggregationConfiguration.get('interval.durationSec').valueChanges + ).pipe( + takeUntilDestroyed() + ).subscribe(() => { + this.checkProduceIntermediate(); + }); + this.entityAggregationConfiguration.valueChanges.pipe( takeUntilDestroyed() ).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { @@ -174,6 +185,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); this.updatedOffsetHint(); setTimeout(() => { this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); @@ -194,6 +206,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetSec').value); this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); + this.checkProduceIntermediate(); } } @@ -255,16 +268,49 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor } } + private checkProduceIntermediate() { + const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; + let durationSec = 0; + switch (intervalType) { + case AggIntervalType.CUSTOM: + durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; + break + case AggIntervalType.HOUR: + durationSec = HOUR / SECOND; + break + case AggIntervalType.DAY: + durationSec = DAY / SECOND; + break + case AggIntervalType.WEEK: + case AggIntervalType.WEEK_SUN_SAT: + durationSec = WEEK / SECOND; + break + case AggIntervalType.MONTH: + durationSec = AVG_MONTH / SECOND; + break + case AggIntervalType.QUARTER: + durationSec = AVG_QUARTER / SECOND; + break + case AggIntervalType.YEAR: + durationSec = YEAR / SECOND; + break + } + if (durationSec > this.intermediateAggregationIntervalInSecForCF) { + this.entityAggregationConfiguration.get('produceIntermediateResult').enable({emitEvent: false}); + } else { + this.entityAggregationConfiguration.get('produceIntermediateResult').disable({emitEvent: false}); + } + } + private updatedOffsetHint(): void { const offset = this.entityAggregationConfiguration.get('interval.offsetSec').value; const intervalType = this.entityAggregationConfiguration.get('interval.type').value as AggIntervalType; const durationSec = this.entityAggregationConfiguration.get('interval.durationSec').value; const offsetCategory = this.getTimeCategory(offset); const now = _moment.utc(); - let interval: string = ''; + let interval: string; if (intervalType === AggIntervalType.CUSTOM) { - const durationSecCategory = this.getTimeCategory(durationSec); - const formatString = this.getCustomFormatString(offsetCategory, durationSecCategory); + const formatString = this.getCustomFormatString(offsetCategory, this.getTimeCategory(durationSec)); const intervals: string[] = []; let allInterval = durationSec >= HOUR*6/SECOND && durationSec < DAY/SECOND; now.startOf('year').add(offset, 'seconds'); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 4c311333ec..8a93423e4d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -368,6 +368,34 @@
+
+ + tenant-profile.intermediate-aggregation-interval + + + {{ 'tenant-profile.intermediate-aggregation-interval-required' | translate}} + + + {{ 'tenant-profile.intermediate-aggregation-interval-range' | translate}} + + + + + tenant-profile.reevaluation-check-interval + + + {{ 'tenant-profile.reevaluation-check-interval-required' | translate}} + + + {{ 'tenant-profile.reevaluation-check-interval-range' | translate}} + + + +
tenant-profile.relation-search-entity-limit @@ -526,6 +554,22 @@
+
+ + tenant-profile.alarms-reevaluation-interval + + + {{ 'tenant-profile.alarms-reevaluation-interval-required' | translate}} + + + {{ 'tenant-profile.alarms-reevaluation-interval-range' | translate}} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index a61a1aa1f8..109ec3d3d8 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -120,6 +120,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA minAllowedAggregationIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], maxRelatedEntitiesToReturnPerCfArgument: [1, [Validators.required, Validators.min(1)]], minAllowedScheduledUpdateIntervalInSecForCF: [0, [Validators.required, Validators.min(0)]], + intermediateAggregationIntervalInSecForCF: [0, [Validators.required, Validators.min(1)]], + cfReevaluationCheckInterval: [0, [Validators.required, Validators.min(1)]], + alarmsReevaluationInterval: [0, [Validators.required, Validators.min(1)]], maxDataPointsPerRollingArg: [0, [Validators.required, Validators.min(0)]], maxStateSizeInKBytes: [0, [Validators.required, Validators.min(0)]], calculatedFieldDebugEventsRateLimit: [''], diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index bb9b1b947a..a21d12dc05 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -168,6 +168,7 @@ export interface CalculatedFieldEntityAggregationConfiguration { metrics: Record; interval: AggInterval; watermark?: WatermarkConfig; + produceIntermediateResult?: boolean; output: CalculatedFieldOutput & { decimalsByDefault?: number; }; } diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index b8f04250ce..e1fb04e982 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -111,6 +111,10 @@ export interface DefaultTenantProfileConfiguration { minAllowedAggregationIntervalInSecForCF: number; maxRelatedEntitiesToReturnPerCfArgument: number; minAllowedScheduledUpdateIntervalInSecForCF: number; + intermediateAggregationIntervalInSecForCF: number; + cfReevaluationCheckInterval: number; + alarmsReevaluationInterval: number; + maxDataPointsPerRollingArg: number; maxStateSizeInKBytes: number; maxSingleValueArgumentSizeInKBytes: number; @@ -180,6 +184,9 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan minAllowedAggregationIntervalInSecForCF: 60, maxRelatedEntitiesToReturnPerCfArgument: 100, minAllowedScheduledUpdateIntervalInSecForCF: 0, + intermediateAggregationIntervalInSecForCF: 300, + cfReevaluationCheckInterval: 60, + alarmsReevaluationInterval: 60, maxStateSizeInKBytes: 32, maxSingleValueArgumentSizeInKBytes: 2, calculatedFieldDebugEventsRateLimit: '' diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1229d2233b..fb995e86cb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1316,7 +1316,9 @@ "duration": "Duration", "duration-required": "Duration is required", "duration-min": "Duration should be at least 1 minute", - "duration-hint": "How long to wait for delayed data after the interval ends" + "duration-hint": "How long to wait for delayed data after the interval ends", + "produce-intermediate-result": "Produce intermediate result", + "produce-intermediate-result-hint": "Calculates metrics during the current interval to produce an intermediate result. Updates occur no more often than once every {{ time }}." }, "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", @@ -6273,6 +6275,15 @@ "min-allowed-deduplication-interval": "Min allowed deduplication interval (seconds)", "min-allowed-deduplication-interval-range": "Min allowed deduplication interval value can't be negative", "min-allowed-deduplication-interval-required": "Min allowed deduplication interval is required", + "intermediate-aggregation-interval": "Intermediate aggregation interval (seconds)", + "intermediate-aggregation-interval-range": "Intermediate aggregation interval value can't be less than '1'", + "intermediate-aggregation-interval-required": "Intermediate aggregation interval is required", + "reevaluation-check-interval": "Reevaluation check interval (seconds)", + "reevaluation-check-interval-range": "Reevaluation check interval value can't be less than '1'", + "reevaluation-check-interval-required": "Reevaluation check interval is required", + "alarms-reevaluation-interval": "Alarms reevaluation interval (seconds)", + "alarms-reevaluation-interval-range": "Alarms reevaluation interval value can't be less than '1'", + "alarms-reevaluation-interval-required": "Alarms reevaluation interval is required", "min-allowed-aggregation-interval": "Min allowed aggregation interval (seconds)", "min-allowed-aggregation-interval-range": "Min allowed aggregation interval value can't be negative", "min-allowed-aggregation-interval-required": "Min allowed aggregation interval is required", From 7aa8f0251e4de5bdbc8b15bb75629a4f078a4e66 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 5 Dec 2025 16:28:52 +0200 Subject: [PATCH 13/21] Refactoring for updating alarm rule arguments --- .../ctx/state/BaseCalculatedFieldState.java | 13 +++-- .../ctx/state/SingleValueArgumentEntry.java | 4 +- .../alarm/AlarmCalculatedFieldState.java | 14 ++++++ .../cf/ctx/state/alarm/AlarmRuleState.java | 2 +- .../thingsboard/server/cf/AlarmRulesTest.java | 50 +++++++++++++++++++ 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 95a524187a..c7c630c3b3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -87,16 +87,15 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, if (existingEntry == null || newEntry.isForceResetPrevious()) { validateNewEntry(key, newEntry); - if (existingEntry instanceof RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry) { - relatedEntitiesArgumentEntry.updateEntry(newEntry); - } else if (existingEntry instanceof EntityAggregationArgumentEntry entityAggArgumentEntry) { - entityAggArgumentEntry.updateEntry(newEntry); + if (existingEntry instanceof RelatedEntitiesArgumentEntry || + existingEntry instanceof EntityAggregationArgumentEntry) { + updateEntry(existingEntry, newEntry); } else { arguments.put(key, newEntry); } entryUpdated = true; } else { - entryUpdated = existingEntry.updateEntry(newEntry); + entryUpdated = updateEntry(existingEntry, newEntry); } if (entryUpdated) { @@ -116,6 +115,10 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, return updatedArguments; } + protected boolean updateEntry(ArgumentEntry existingEntry, ArgumentEntry newEntry) { + return existingEntry.updateEntry(newEntry); + } + @Override public void reset() { // must reset everything dependent on arguments requiredArguments = null; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java index 97916192b5..4d0c4d7724 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java @@ -162,9 +162,7 @@ public class SingleValueArgumentEntry implements ArgumentEntry { public boolean updateEntry(ArgumentEntry entry) { if (entry instanceof SingleValueArgumentEntry singleValueEntry) { if (singleValueEntry.getTs() < this.ts) { - if (!isDefaultValue()) { - return false; - } + return false; } Long newVersion = singleValueEntry.getVersion(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java index bd2c272bca..18629bd370 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -225,6 +225,20 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { .build()); } + @Override + protected boolean updateEntry(ArgumentEntry existingArgumentEntry, ArgumentEntry newArgumentEntry) { + if (!(existingArgumentEntry instanceof SingleValueArgumentEntry existingEntry) || + !(newArgumentEntry instanceof SingleValueArgumentEntry newEntry)) { + return super.updateEntry(existingArgumentEntry, newArgumentEntry); + } + if (newEntry.getTs() < existingEntry.getTs()) { + if (existingEntry.isDefaultValue()) { + existingEntry.setTs(newEntry.getTs()); + } + } + return super.updateEntry(existingEntry, newEntry); + } + public void processAlarmAction(Alarm alarm, ActionType action) { switch (action) { case ALARM_ACK -> processAlarmAck(alarm); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java index c6a5cbf418..569bbe9310 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java @@ -157,7 +157,7 @@ public class AlarmRuleState { private AlarmEvalResult evalDuration(CalculatedFieldCtx ctx) { if (eval(condition.getExpression(), ctx)) { long ts = System.currentTimeMillis(); - if (firstEventTs == 0) { + if (firstEventTs <= 0) { firstEventTs = state.getLatestTimestamp(); } lastCheckTs = ts; diff --git a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java index 5f91dab190..5959d98442 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -194,6 +194,30 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + @Test + public void testCreateAlarm_eventBeforeDefaultTs() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"values\": {\"temperature\": 50}, \"ts\": " + (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30) + "}")); + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + @Test public void testCreateAlarm_repeatingCondition() throws Exception { Argument temperatureArgument = new Argument(); @@ -350,6 +374,32 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + @Test + public void testCreateAlarm_durationCondition_defaultValue() { + Argument powerConsumptionArgument = new Argument(); + powerConsumptionArgument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + powerConsumptionArgument.setDefaultValue("3500"); + Map arguments = Map.of( + "powerConsumption", powerConsumptionArgument + ); + + long createDurationMs = 2000L; + Map createRules = Map.of( + AlarmSeverity.CRITICAL, new Condition("return powerConsumption >= 3000;", null, null, + new AlarmConditionValue(2000L, null), null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 2 seconds", + arguments, createRules, null); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + assertThat(alarmResult.getConditionDuration()).isBetween(createDurationMs, createDurationMs + 2000); + }); + } + @Test public void testCreateAlarm_currentOwnerArgument() throws Exception { Argument temperatureArgument = new Argument(); From aeb9958bf53c139098fc5245f2df3e28af5d44ca Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 5 Dec 2025 18:48:29 +0200 Subject: [PATCH 14/21] Added string autocomplete for telemetry device tab (#14507) * added string autocomplete for telemetry device tab * fixed validation * fixed button alignment * fixed styles --- .../add-attribute-dialog.component.html | 22 ++++---- .../add-attribute-dialog.component.ts | 33 ++++++++++-- .../attribute/attribute-table.component.ts | 14 ++++-- .../string-autocomplete.component.html | 8 +-- .../string-autocomplete.component.scss | 20 -------- .../string-autocomplete.component.ts | 50 ++++++++++++++----- 6 files changed, 93 insertions(+), 54 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index cd87bde8f2..0dc4e0a90f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -30,16 +30,18 @@
- - attribute.key - - - {{ (isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required') | translate }} - - - {{ 'attribute.key-max-length' | translate }} - - + + diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts index 1336236442..3dd1a09a8c 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.ts @@ -23,13 +23,23 @@ import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Valida import { EntityId } from '@shared/models/id/entity-id'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; -import { AttributeData, AttributeScope, LatestTelemetry, TelemetryType } from '@shared/models/telemetry/telemetry.models'; +import { + AttributeData, + AttributeScope, + LatestTelemetry, + TelemetryType +} from '@shared/models/telemetry/telemetry.models'; import { AttributeService } from '@core/http/attribute.service'; import { Observable } from 'rxjs'; +import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; +import { map } from 'rxjs/operators'; +import { ErrorMessageConfig } from '@shared/components/string-autocomplete.component'; +import { TranslateService } from '@ngx-translate/core'; export interface AddAttributeDialogData { entityId: EntityId; attributeScope: TelemetryType; + datasource?: AttributeDatasource; } @Component({ @@ -47,19 +57,22 @@ export class AddAttributeDialogComponent extends DialogComponent, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AddAttributeDialogData, private attributeService: AttributeService, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, - public fb: FormBuilder) { + public fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); } ngOnInit(): void { this.attributeFormGroup = this.fb.group({ - key: ['', [Validators.required, Validators.maxLength(255)]], + key: ['', this.keyValidators], value: [null, [Validators.required]] }); this.isTelemetry = this.data.attributeScope === LatestTelemetry.LATEST_TELEMETRY; @@ -97,4 +110,18 @@ export class AddAttributeDialogComponent extends DialogComponent this.dialogRef.close(true)); } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return this.data.datasource?.getAllAttributes(this.data.entityId,this.data.attributeScope).pipe( + map(attributes => attributes?.filter(attribute => attribute.key.toLowerCase().includes(search)).map(a => a.key)), + ) + } + + get attributeErrorMessages(): ErrorMessageConfig { + return { + required: this.translate.instant(this.isTelemetry ? 'attribute.telemetry-key-required' : 'attribute.key-required'), + maxlength: this.translate.instant('attribute.key-max-length') + } + } } diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index bc9059d80a..c81c33f741 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -317,13 +317,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if ($event) { $event.stopPropagation(); } + const data: AddAttributeDialogData = { + entityId: this.entityIdValue, + attributeScope: this.attributeScope, + }; + + if(this.attributeScope === LatestTelemetry.LATEST_TELEMETRY) { + data.datasource = this.dataSource; + } + this.dialog.open(AddAttributeDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - entityId: this.entityIdValue, - attributeScope: this.attributeScope - } + data }).afterClosed().subscribe( (res) => { if (res) { diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.html b/ui-ngx/src/app/shared/components/string-autocomplete.component.html index 31966b932d..0bc768d057 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.html @@ -31,21 +31,21 @@ warning - {{errorText}} + {{ getErrorMessage }} - + diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.scss b/ui-ngx/src/app/shared/components/string-autocomplete.component.scss index 311c04bca8..87d8bd6732 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.scss +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.scss @@ -25,25 +25,5 @@ margin-right: 8px; } } - - .tb-autocomplete.tb-option-input-autocomplete { - .mat-mdc-option { - border-bottom: none; - - .mdc-list-item__primary-text { - flex: 1; - display: flex; - flex-direction: row; - gap: 8px; - - .tb-option { - font-size: 14px; - font-weight: 400; - line-height: 20px; - letter-spacing: 0.2px; - } - } - } - } } } diff --git a/ui-ngx/src/app/shared/components/string-autocomplete.component.ts b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts index d07fd2d4c3..4f78a8e939 100644 --- a/ui-ngx/src/app/shared/components/string-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/string-autocomplete.component.ts @@ -14,27 +14,25 @@ /// limitations under the License. /// -import { - Component, - Input, - forwardRef, - OnInit, - ViewChild, - ElementRef -} from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, - NG_VALUE_ACCESSOR, + FormBuilder, FormControl, - Validators, - FormBuilder + NG_VALUE_ACCESSOR, + ValidatorFn, + Validators } from '@angular/forms'; import { Observable, of } from 'rxjs'; -import { tap, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { coerceBoolean } from '@shared/decorators/coercion'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +export interface ErrorMessageConfig { + [errorKey: string]: string; +} + @Component({ selector: 'tb-string-autocomplete', templateUrl: './string-autocomplete.component.html', @@ -85,6 +83,12 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit @Input() errorText: string; + @Input() + controlValidators: ValidatorFn[] = []; + + @Input() + errorMessages: ErrorMessageConfig; + @Input() @coerceBoolean() showInlineError = false; @@ -107,7 +111,13 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit ngOnInit() { const validators = [Validators.pattern(/.*\S.*/)]; - if (this.required) { + if (this.controlValidators?.length) { + validators.push(...this.controlValidators); + const parentHasRequired = this.controlValidators.some(v => v === Validators.required); + if (this.required && !parentHasRequired) { + validators.push(Validators.required); + } + } else if (this.required) { validators.push(Validators.required); } this.selectionFormControl = this.fb.control('', validators); @@ -184,4 +194,18 @@ export class StringAutocompleteComponent implements ControlValueAccessor, OnInit this.nameInput.nativeElement.focus(); }, 0); } + + get getErrorMessage(): string { + if (!this.selectionFormControl.errors) { + return ''; + } + if (this.errorMessages) { + for (const errorKey in this.selectionFormControl.errors) { + if (this.errorMessages[errorKey]) { + return this.errorMessages[errorKey]; + } + } + } + return this.errorText; + } } From 30a0200e35cfeb12267e09203acd71c30be52dda Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 8 Dec 2025 08:51:34 +0200 Subject: [PATCH 15/21] update ctx when produceIntermediateResult changes --- .../server/service/cf/ctx/state/CalculatedFieldCtx.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 020c19c4b5..a9a4a06c60 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -673,7 +673,8 @@ public class CalculatedFieldCtx implements Closeable { && other.getCalculatedField().getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration otherConfig) { boolean metricsChanged = !Objects.equals(thisConfig.getMetrics(), otherConfig.getMetrics()); boolean watermarkChanged = !Objects.equals(thisConfig.getWatermark(), otherConfig.getWatermark()); - return metricsChanged || watermarkChanged; + boolean produceIntermediateResultChanged = thisConfig.isProduceIntermediateResult() != otherConfig.isProduceIntermediateResult(); + return metricsChanged || watermarkChanged || produceIntermediateResultChanged; } return false; } From 4a87a76f7dbbb319f94a45a20ce1c4b5918a7643 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 8 Dec 2025 10:12:19 +0200 Subject: [PATCH 16/21] UI: Add error and progress bar for 2fa cards --- .../login/force-two-factor-auth-login.component.html | 11 +++++++---- .../login/force-two-factor-auth-login.component.scss | 6 ++++++ .../login/force-two-factor-auth-login.component.ts | 8 ++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 94bd0b2cf0..24f9ec1bd7 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -15,8 +15,11 @@ limitations under the License. --> -