From 71afb3a438c5b4212dd04ec2e00627e903831e8f Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 21 Nov 2025 11:53:32 +0200 Subject: [PATCH 01/10] Fix alarm rule propagation settings update --- .../server/service/cf/ctx/state/CalculatedFieldCtx.java | 3 +++ .../configuration/AlarmCalculatedFieldConfiguration.java | 7 +++++++ 2 files changed, 10 insertions(+) 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 393851eb02..76872f731d 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 @@ -640,6 +640,9 @@ public class CalculatedFieldCtx implements Closeable { // if the rules have any changes not tracked by hasStateChanges return true; } + if (!thisConfig.propagationSettingsEqual(otherConfig)) { + return true; + } } if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) { return true; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java index d36ba33849..6f3cdbdd3c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java @@ -87,4 +87,11 @@ public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculat }); } + public boolean propagationSettingsEqual(AlarmCalculatedFieldConfiguration other) { + return this.propagate == other.propagate && + this.propagateToOwner == other.propagateToOwner && + this.propagateToTenant == other.propagateToTenant && + Objects.equals(this.propagateRelationTypes, other.propagateRelationTypes); + } + } From 615f9d284c1221c495544560374225ba30faf52f Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 24 Nov 2025 10:34:27 +0200 Subject: [PATCH 02/10] Alarm rules: no-data filter predicate --- ...alculatedFieldManagerMessageProcessor.java | 2 +- .../cf/ctx/state/CalculatedFieldCtx.java | 5 ++- .../alarm/AlarmCalculatedFieldState.java | 12 ++++++ .../common/data/alarm/rule/AlarmRule.java | 2 +- .../alarm/rule/condition/AlarmCondition.java | 5 +++ .../expression/AlarmConditionExpression.java | 5 +++ .../expression/AlarmConditionFilter.java | 19 ++++++++- .../SimpleAlarmConditionExpression.java | 6 +++ .../predicate/FilterPredicateType.java | 1 + .../predicate/KeyFilterPredicate.java | 4 +- .../predicate/NoDataFilterPredicate.java | 39 +++++++++++++++++++ 11 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java 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 dc6757f88f..356d66e21c 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 @@ -179,7 +179,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware cfsReevaluationTask = systemContext.getScheduler().scheduleWithFixedDelay(() -> { try { calculatedFields.values().forEach(cf -> { - if (cf.isRequiresScheduledReevaluation()) { + if (cf.requiresScheduledReevaluation()) { applyToTargetCfEntityActors(cf, TbCallback.EMPTY, (entityId, callback) -> { log.debug("[{}][{}] Pushing scheduled CF reevaluate msg", entityId, cf.getCfId()); getOrCreateActor(entityId).tell(new CalculatedFieldReevaluateMsg(tenantId, cf)); 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 76872f731d..a196bbe4b7 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 @@ -210,7 +210,7 @@ public class CalculatedFieldCtx implements Closeable { this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; } - public boolean isRequiresScheduledReevaluation() { + public boolean requiresScheduledReevaluation() { long now = System.currentTimeMillis(); if (calculatedField.getConfiguration() instanceof EntityAggregationCalculatedFieldConfiguration entityAggregationConfig) { Watermark watermark = entityAggregationConfig.getWatermark(); @@ -230,8 +230,9 @@ public class CalculatedFieldCtx implements Closeable { } boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { - long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); + // TODO: handle reeaval for no-data conditions! if (requiresScheduledReevaluation) { + long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); if (now - lastReevaluationTs >= reevaluationIntervalMillis) { lastReevaluationTs = now; return true; 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 342f7534c2..8823ca397f 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 @@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAl import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.BooleanFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; import org.thingsboard.server.common.data.audit.ActionType; @@ -428,6 +429,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { case STRING -> evalStrPredicate(argument, (StringFilterPredicate) predicate); case NUMERIC -> evalNumPredicate(argument, (NumericFilterPredicate) predicate); case BOOLEAN -> evalBooleanPredicate(argument, (BooleanFilterPredicate) predicate); + case NO_DATA -> evalNoDataPredicate(argument, (NoDataFilterPredicate) predicate); case COMPLEX -> evalComplexPredicate(argument, (ComplexFilterPredicate) predicate); }; } @@ -512,6 +514,16 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { }; } + private boolean evalNoDataPredicate(SingleValueArgumentEntry argument, NoDataFilterPredicate predicate) { + long passedMs = System.currentTimeMillis() - argument.getTs(); + long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue); + if (duration > 0) { + return passedMs >= predicate.getUnit().toMillis(duration); + } else { + return false; + } + } + protected T resolveValue(AlarmConditionValue conditionValue, Function mapper) { T value = conditionValue.getStaticValue(); if (value == null) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java index 9a4e875154..23dbc8fede 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java @@ -33,7 +33,7 @@ public class AlarmRule { @JsonIgnore public boolean requiresScheduledReevaluation() { - return condition.hasSchedule(); + return condition.requiresScheduledReevaluation(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java index 9bb549994b..96d63f78d3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -50,6 +50,11 @@ public abstract class AlarmCondition { return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); } + @JsonIgnore + public boolean requiresScheduledReevaluation() { + return hasSchedule() || expression.requiresScheduledReevaluation(); + } + @JsonIgnore public abstract AlarmConditionType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java index e855f8efd3..0502a10105 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionExpression.java @@ -32,4 +32,9 @@ public interface AlarmConditionExpression { @JsonIgnore AlarmConditionExpressionType getType(); + @JsonIgnore + default boolean requiresScheduledReevaluation() { + return false; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java index e99849ea82..4c6df3825d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java @@ -17,8 +17,11 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.ComplexFilterPredicate; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.KeyFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; @@ -34,7 +37,21 @@ public class AlarmConditionFilter implements Serializable { private EntityKeyValueType valueType; private ComplexOperation operation; @Valid - @NotNull + @NotEmpty private List predicates; + public boolean hasPredicate(FilterPredicateType type) { + return containsPredicate(predicates, type); + } + + private boolean containsPredicate(List predicates, FilterPredicateType type) { + return predicates.stream().anyMatch(predicate -> { + if (predicate instanceof ComplexFilterPredicate complexPredicate) { + return containsPredicate(complexPredicate.getPredicates(), type); + } else { + return predicate.getType() == type; + } + }); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java index 8c27400961..b0afbcc7ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/SimpleAlarmConditionExpression.java @@ -20,6 +20,7 @@ import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.FilterPredicateType; import java.util.List; @@ -38,4 +39,9 @@ public class SimpleAlarmConditionExpression implements AlarmConditionExpression return AlarmConditionExpressionType.SIMPLE; } + @Override + public boolean requiresScheduledReevaluation() { + return filters.stream().anyMatch(filter -> filter.hasPredicate(FilterPredicateType.NO_DATA)); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java index af7c45ac5b..687b39932e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/FilterPredicateType.java @@ -19,5 +19,6 @@ public enum FilterPredicateType { STRING, NUMERIC, BOOLEAN, + NO_DATA, COMPLEX } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java index 58355c627d..ca4531c123 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/KeyFilterPredicate.java @@ -27,7 +27,9 @@ import java.io.Serializable; @Type(value = StringFilterPredicate.class, name = "STRING"), @Type(value = NumericFilterPredicate.class, name = "NUMERIC"), @Type(value = BooleanFilterPredicate.class, name = "BOOLEAN"), - @Type(value = ComplexFilterPredicate.class, name = "COMPLEX")}) + @Type(value = NoDataFilterPredicate.class, name = "NO_DATA"), + @Type(value = ComplexFilterPredicate.class, name = "COMPLEX") +}) public interface KeyFilterPredicate extends Serializable { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java new file mode 100644 index 0000000000..11a014fd44 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; + +import java.util.concurrent.TimeUnit; + +@Data +public class NoDataFilterPredicate implements KeyFilterPredicate { + + @NotNull + private TimeUnit unit; + @Valid + @NotNull + private AlarmConditionValue duration; + + @Override + public FilterPredicateType getType() { + return FilterPredicateType.NO_DATA; + } + +} From 3922788c837a9c3d938814d048867b7c3a4b2ad3 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 24 Nov 2025 16:24:25 +0200 Subject: [PATCH 03/10] Handling of no-data alarm rule predicate --- .../cf/ctx/state/CalculatedFieldCtx.java | 1 - .../alarm/AlarmCalculatedFieldState.java | 45 +++++++++------ .../cf/ctx/state/alarm/AlarmEvalResult.java | 10 ++++ .../cf/ctx/state/alarm/AlarmRuleState.java | 27 +++++---- .../thingsboard/server/cf/AlarmRulesTest.java | 56 +++++++++++++++++++ .../alarm/rule/condition/AlarmCondition.java | 10 ++++ .../predicate/NoDataFilterPredicate.java | 4 ++ .../predicate/NumericFilterPredicate.java | 4 ++ 8 files changed, 128 insertions(+), 29 deletions(-) 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 a196bbe4b7..01f5fbd950 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 @@ -230,7 +230,6 @@ public class CalculatedFieldCtx implements Closeable { } boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { - // TODO: handle reeaval for no-data conditions! if (requiresScheduledReevaluation) { long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); if (now - lastReevaluationTs >= reevaluationIntervalMillis) { 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 8823ca397f..bd2c272bca 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 @@ -71,6 +71,8 @@ import java.util.function.Function; import static org.thingsboard.server.common.data.StringUtils.equalsAny; import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.NEW_EVENT; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Cause.SCHEDULED_REEVALUATION; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.FALSE; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.NOT_YET_TRUE; import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; @@ -213,9 +215,9 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { state.setDurationCheckFuture(future); } } - return evalResult; + return evalResult.withCause(NEW_EVENT); } else { - return state.reeval(System.currentTimeMillis(), ctx); + return state.reeval(System.currentTimeMillis(), ctx).withCause(SCHEDULED_REEVALUATION); } }, ctx); return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() @@ -248,12 +250,12 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { private TbAlarmResult createOrClearAlarms(Function evalFunction, CalculatedFieldCtx ctx) { - TbAlarmResult result = null; + AlarmEvalResult evalResult = null; AlarmRuleState resultState = null; AlarmRuleState.StateInfo resultStateInfo = null; for (AlarmRuleState state : createRuleStates.values()) { - AlarmEvalResult evalResult = evalFunction.apply(state); + evalResult = evalFunction.apply(state); log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult); if (evalResult.getStatus() == TRUE) { resultState = state; @@ -263,13 +265,14 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { } } + TbAlarmResult result = null; if (resultState != null) { - result = calculateAlarmResult(resultState, ctx); + result = calculateAlarmResult(resultState, evalResult, ctx); resultStateInfo = resultState.getStateInfo(); log.debug("Alarm result for state {}: {}", resultState, result); clearState(clearRuleState); } else if (currentAlarm != null && clearRuleState != null) { - AlarmEvalResult evalResult = evalFunction.apply(clearRuleState); + evalResult = evalFunction.apply(clearRuleState); log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult); if (evalResult.getStatus() == TRUE) { resultStateInfo = clearRuleState.getStateInfo(); @@ -316,21 +319,25 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { } } - private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, CalculatedFieldCtx ctx) { + private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, AlarmEvalResult evalResult, CalculatedFieldCtx ctx) { AlarmSeverity severity = ruleState.getSeverity(); if (currentAlarm != null) { - currentAlarm.setEndTs(System.currentTimeMillis()); AlarmSeverity oldSeverity = currentAlarm.getSeverity(); - // Skip update if severity is decreased. - if (severity.ordinal() <= oldSeverity.ordinal()) { - currentAlarm.setDetails(createDetails(ruleState)); - currentAlarm.setSeverity(severity); - AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); - currentAlarm = result.getAlarm(); - return TbAlarmResult.fromAlarmResult(result); - } else { + if (severity.ordinal() > oldSeverity.ordinal()) { + log.trace("Skipping alarm update for result state {} for eval result {} because severity is decreased", ruleState, evalResult); + return null; + } + if (severity.ordinal() == oldSeverity.ordinal() && evalResult.getCause() == SCHEDULED_REEVALUATION) { + log.trace("Skipping alarm update for result state {} for eval result {}", ruleState, evalResult); return null; } + + currentAlarm.setEndTs(System.currentTimeMillis()); + currentAlarm.setDetails(createDetails(ruleState)); + currentAlarm.setSeverity(severity); + AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); } else { var newAlarm = new Alarm(); newAlarm.setType(alarmType); @@ -339,7 +346,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { newAlarm.setSeverity(severity); long startTs = latestTimestamp; long currentTime = System.currentTimeMillis(); - if (startTs == 0L || startTs > currentTime) { + if (startTs <= 0L || startTs > currentTime) { startTs = currentTime; } newAlarm.setStartTs(startTs); @@ -518,7 +525,9 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { long passedMs = System.currentTimeMillis() - argument.getTs(); long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue); if (duration > 0) { - return passedMs >= predicate.getUnit().toMillis(duration); + long requiredDuration = predicate.getUnit().toMillis(duration); + log.trace("[{}] No data for argument {} during {} ms, required duration: {} ms", ctx, argument, passedMs, requiredDuration); + return passedMs >= requiredDuration; } else { return false; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java index 4f1a8638ee..2569f837fa 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java @@ -30,6 +30,7 @@ public class AlarmEvalResult { private final Status status; private final long leftDuration; private final long leftEvents; + private Cause cause; public AlarmEvalResult(Status status) { this(status, 0, 0); @@ -39,8 +40,17 @@ public class AlarmEvalResult { return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents); } + public AlarmEvalResult withCause(Cause cause) { + this.cause = cause; + return this; + } + public enum Status { FALSE, NOT_YET_TRUE, TRUE; } + public enum Cause { + NEW_EVENT, SCHEDULED_REEVALUATION; + } + } 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 4c189887b4..c6a5cbf418 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 @@ -43,6 +43,8 @@ import java.time.ZonedDateTime; import java.util.Optional; import java.util.concurrent.ScheduledFuture; +import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE; + @Data @Slf4j public class AlarmRuleState { @@ -81,16 +83,20 @@ public class AlarmRuleState { boolean active = isActive(ts); switch (condition.getType()) { case SIMPLE, REPEATING -> { - if (this.active == null || active != this.active) { - this.active = active; - if (active) { - return doEval(false, ctx); - } + boolean activeChanged = this.active == null || active != this.active; + this.active = active; + if (!active) { + return AlarmEvalResult.EMPTY; } - if (active) { - return AlarmEvalResult.NOT_YET_TRUE; - } else { - return AlarmEvalResult.FALSE; + + if ((condition.hasSchedule() && activeChanged) || + condition.getExpression().requiresScheduledReevaluation()) { + AlarmEvalResult result = doEval(false, ctx); + if (result.getStatus() == TRUE) { + return result; + } else { + return AlarmEvalResult.EMPTY; + } } } case DURATION -> { @@ -116,7 +122,7 @@ public class AlarmRuleState { } } } - return AlarmEvalResult.FALSE; + return AlarmEvalResult.EMPTY; } public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) { @@ -338,6 +344,7 @@ public class AlarmRuleState { } public record StateInfo(Long eventCount, Long duration) { + static final StateInfo EMPTY = new StateInfo(null, null); } 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 2ac5d59b3a..5f91dab190 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -42,6 +42,7 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmC import org.thingsboard.server.common.data.alarm.rule.condition.expression.ComplexOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NoDataFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation; import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.StringFilterPredicate; @@ -475,6 +476,61 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + @Test + public void testCreateAlarm_noDataPredicate() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + long majorNoDataDuration = 3L; + long criticalNoDataDuration = 10L; + + SimpleAlarmConditionExpression majorExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter majorFilter = new AlarmConditionFilter(); + majorFilter.setArgument("temperature"); + majorFilter.setValueType(EntityKeyValueType.NUMERIC); + majorFilter.setPredicates(List.of( + new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)), + new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue(majorNoDataDuration, null)) + )); + majorExpression.setFilters(List.of(majorFilter)); + + SimpleAlarmConditionExpression criticalExpression = new SimpleAlarmConditionExpression(); + AlarmConditionFilter criticalFilter = new AlarmConditionFilter(); + criticalFilter.setArgument("temperature"); + criticalFilter.setValueType(EntityKeyValueType.NUMERIC); + criticalFilter.setPredicates(List.of( + new NumericFilterPredicate(NumericOperation.GREATER, new AlarmConditionValue<>(25.0, null)), + new NoDataFilterPredicate(TimeUnit.SECONDS, new AlarmConditionValue(criticalNoDataDuration, null)) + )); + criticalExpression.setFilters(List.of(criticalFilter)); + + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition(majorExpression, null, null), + AlarmSeverity.CRITICAL, new Condition(criticalExpression, null, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "No Temperature Alarm", + arguments, createRules, null); + + postTelemetry(deviceId, "{\"temperature\":50}"); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isCreated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.MAJOR); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + + checkAlarmResult(calculatedField, alarmResult -> { + assertThat(alarmResult.isSeverityUpdated()).isTrue(); + assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); + assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); + }); + } + @Test public void testChangeAlarmType() throws Exception { Argument temperatureArgument = new Argument(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java index 96d63f78d3..b4e1446798 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; import lombok.Data; import lombok.NoArgsConstructor; import org.jetbrains.annotations.NotNull; @@ -55,6 +56,15 @@ public abstract class AlarmCondition { return hasSchedule() || expression.requiresScheduledReevaluation(); } + @JsonIgnore + @AssertTrue(message = "Expressions requiring scheduled reevaluation can only be used with simple alarm conditions") + public boolean isValid() { + if (getType() != AlarmConditionType.SIMPLE && expression.requiresScheduledReevaluation()) { + return false; + } + return true; + } + @JsonIgnore public abstract AlarmConditionType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java index 11a014fd44..82b366f6ae 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java @@ -17,12 +17,16 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import java.util.concurrent.TimeUnit; @Data +@AllArgsConstructor +@NoArgsConstructor public class NoDataFilterPredicate implements KeyFilterPredicate { @NotNull diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java index 65316eda88..4bc547695b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java @@ -17,10 +17,14 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression.predi import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; @Data +@AllArgsConstructor +@NoArgsConstructor public class NumericFilterPredicate implements SimpleKeyFilterPredicate { @NotNull From 1a9b5eaae1ecc8183ab53b72f8a49a9dbf064583 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 25 Nov 2025 13:04:02 +0200 Subject: [PATCH 04/10] Decrease alarm rules reevaluation interval --- .../java/org/thingsboard/server/actors/ActorSystemContext.java | 2 +- application/src/main/resources/thingsboard.yml | 2 +- 2 files changed, 2 insertions(+), 2 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 f38a1972c0..0a31f0b981 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -673,7 +673,7 @@ public class ActorSystemContext { @Getter private long cfCheckInterval; - @Value("${actors.alarms.reevaluation_interval:120}") + @Value("${actors.alarms.reevaluation_interval:60}") @Getter private long alarmRulesReevaluationInterval; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4ffa7f31d2..d3d04bcea0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -542,7 +542,7 @@ actors: 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. - reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:120}" + reevaluation_interval: "${ACTORS_ALARMS_REEVALUATION_INTERVAL_SEC:60}" debug: settings: From e8bf7b99709fab07a0f7b8eaca4da4150f9623e2 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 25 Nov 2025 13:04:47 +0200 Subject: [PATCH 05/10] Improvements for tests --- .../server/actors/app/AppActor.java | 2 +- .../cf/DefaultCalculatedFieldCache.java | 8 ++++--- .../server/controller/EdqsControllerTest.java | 7 +++++- .../EdqsEntityQueryControllerTest.java | 4 ++-- .../controller/EntityQueryControllerTest.java | 23 +++++++++++-------- .../entitiy/EdqsEntityServiceTest.java | 4 ++-- ...faultTelemetrySubscriptionServiceTest.java | 6 +---- .../cache/TbCaffeineCacheConfiguration.java | 5 ++-- 8 files changed, 33 insertions(+), 26 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index da16e55db8..cfac432017 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -178,7 +178,7 @@ public class AppActor extends ContextAwareActor { }); } } - if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE)) { + if (!msg.getEntityId().getEntityType().isOneOf(EntityType.TENANT_PROFILE, EntityType.TB_RESOURCE, EntityType.USER)) { log.warn("Message has system tenant id: {}", msg); } } else { 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 f2b8d4d9db..e7c4801c12 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 @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageDataIterable; @@ -243,11 +244,12 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { @Override public EntityId getProfileId(TenantId tenantId, EntityId entityId) { - return switch (entityId.getEntityType()) { - case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId(); - case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId(); + HasId profile = switch (entityId.getEntityType()) { + case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId); + case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId); default -> null; }; + return profile != null ? profile.getId() : null; } @Override diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java index 91be3f4744..c3f11ce8d8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsControllerTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; @@ -41,6 +42,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.KeyFilter; import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.ArrayList; import java.util.Collections; @@ -62,8 +64,11 @@ public class EdqsControllerTest extends AbstractControllerTest { @Autowired private JdbcTemplate jdbcTemplate; + @MockitoBean + private EdqsRocksDb edqsRocksDb; + @Before - public void beforeEdqsControllerTest() throws Exception { + public void before() throws Exception { loginTenantAdmin(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 655ab417e6..15ad57b62e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -19,8 +19,8 @@ import org.assertj.core.api.ThrowingConsumer; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; @@ -59,7 +59,7 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { @Autowired private DiscoveryService discoveryService; - @MockBean // so that we don't do backup for tests + @MockitoBean // so that we don't do backup for tests private EdqsRocksDb edqsRocksDb; @Before diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 775df230b6..e32a2e8086 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -23,6 +23,7 @@ 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.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; @@ -71,6 +72,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.ArrayList; import java.util.Arrays; @@ -102,6 +104,9 @@ public class EntityQueryControllerTest extends AbstractControllerTest { @Autowired private QueueStatsService queueStatsService; + @MockitoBean + private EdqsRocksDb edqsRocksDb; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -436,7 +441,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); - AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList(); @@ -446,8 +451,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, alarmFields); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { }); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } @@ -509,16 +514,16 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); - AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); + AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, Collections.emptyList()); - PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); + PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); - AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); - PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { + AlarmDataQuery filteredAssetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, keyFilters, Collections.emptyList()); + PageData filteredAssetAlamData = doPostWithTypedResponse("/api/alarmsQuery/find", filteredAssetAlarmQuery, new TypeReference<>() { }); Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } @@ -574,7 +579,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { EntityTypeFilter deviceTypeFilter = new EntityTypeFilter(); deviceTypeFilter.setEntityType(EntityType.DEVICE); - AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); + AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); PageData alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); List retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); @@ -1291,7 +1296,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { findByQueryAndCheck(query, 0); } - private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { await() .alias("data by query") .atMost(30, TimeUnit.SECONDS) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java index 2f244807bd..610e27944e 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EdqsEntityServiceTest.java @@ -19,8 +19,8 @@ import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; @@ -61,7 +61,7 @@ public class EdqsEntityServiceTest extends EntityServiceTest { @Autowired private EdqsService edqsService; - @MockBean + @MockitoBean private EdqsRocksDb edqsRocksDb; @Before diff --git a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java index 153228a865..3c62333122 100644 --- a/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionServiceTest.java @@ -162,7 +162,7 @@ class DefaultTelemetrySubscriptionServiceTest { apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); lenient().when(apiUsageStateService.getApiUsageState(tenantId)).thenReturn(apiUsageState); - lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId)).thenReturn(tpi); + lenient().when(partitionService.resolve(eq(ServiceType.TB_CORE), eq(tenantId), any())).thenReturn(tpi); lenient().when(tsService.save(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); lenient().when(tsService.saveWithoutLatest(tenantId, entityId, sampleTimeseries, sampleTtl)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), null))); @@ -310,8 +310,6 @@ class DefaultTelemetrySubscriptionServiceTest { given(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).willReturn(immediateFuture(List.of(entityView))); // mock that save latest call for entity view is successful given(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).willReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); - // mock TPI for entity view - given(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).willReturn(tpi); var request = TimeseriesSaveRequest.builder() .tenantId(tenantId) @@ -373,8 +371,6 @@ class DefaultTelemetrySubscriptionServiceTest { lenient().when(tbEntityViewService.findEntityViewsByTenantIdAndEntityIdAsync(tenantId, entityId)).thenReturn(immediateFuture(List.of(entityView))); // mock that save latest call for entity view is successful lenient().when(tsService.saveLatest(tenantId, entityView.getId(), sampleTimeseries)).thenReturn(immediateFuture(TimeseriesSaveResult.of(sampleTimeseries.size(), listOfNNumbers(sampleTimeseries.size())))); - // mock TPI for entity view - lenient().when(partitionService.resolve(ServiceType.TB_CORE, tenantId, entityView.getId())).thenReturn(tpi); var request = TimeseriesSaveRequest.builder() .tenantId(tenantId) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java index 09a363853f..ddefa966f0 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbCaffeineCacheConfiguration.java @@ -70,11 +70,10 @@ public class TbCaffeineCacheConfiguration { } private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { - - final Caffeine caffeineBuilder - = Caffeine.newBuilder() + Caffeine caffeineBuilder = Caffeine.newBuilder() .weigher(collectionSafeWeigher()) .maximumWeight(cacheSpec.getMaxSize()) + .recordStats() .ticker(ticker()); if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) { caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES); From 18b1f882ec6b1c85c9e958effc02f3e33644585d Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 28 Nov 2025 10:20:50 +0200 Subject: [PATCH 06/10] UI: Add missing for option for filters --- .../alarm-rules/alarm-rule.module.ts | 6 +- ...alarm-rule-condition-dialog.component.html | 6 +- ...f-alarm-rule-condition-dialog.component.ts | 32 +++- ...mplex-filter-predicate-dialog.component.ts | 11 +- .../alarm-rule-filter-dialog.component.ts | 2 +- .../alarm-rule-filter-list.component.html | 2 +- ...rm-rule-filter-predicate-list.component.ts | 26 +-- ...ter-predicate-no-data-value.component.html | 77 +++++++++ ...ilter-predicate-no-data-value.component.ts | 159 ++++++++++++++++++ ...rule-filter-predicate-value.component.html | 9 + ...m-rule-filter-predicate-value.component.ts | 26 ++- ...alarm-rule-filter-predicate.component.html | 17 +- .../alarm-rule-filter-predicate.component.ts | 86 +++++++--- .../alarm-rule-filter-text.component.ts | 34 ++-- .../app/shared/models/alarm-rule.models.ts | 94 ++++++++++- .../assets/locale/locale.constant-en_US.json | 8 +- 16 files changed, 519 insertions(+), 76 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html create mode 100644 ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts index c02c11a170..52d91188bc 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts @@ -50,6 +50,9 @@ import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/fil import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; import { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.component"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; +import { + AlarmRuleFilterPredicateNoDataValueComponent +} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component"; @NgModule({ declarations: [ @@ -69,7 +72,8 @@ import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alar AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleDetailsDialogComponent, AlarmRuleFilterConfigComponent, - AlarmRuleTableHeaderComponent + AlarmRuleTableHeaderComponent, + AlarmRuleFilterPredicateNoDataValueComponent ], imports: [ CommonModule, 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 feafcaa0a4..5d94476e6e 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 @@ -78,10 +78,14 @@ alarm-rule.condition-type - + {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} + @if (isNoData) { + alarm-rule.condition-type-hint + } @if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
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 48b290b32c..3635d415d7 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 @@ -28,7 +28,8 @@ import { AlarmRuleCondition, AlarmRuleConditionType, AlarmRuleConditionTypeTranslationMap, - AlarmRuleExpressionType + AlarmRuleExpressionType, + AlarmRuleFilter } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument, @@ -102,6 +103,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent; + isNoData: boolean = false; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, @@ -136,12 +139,20 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { this.updateValidators(type, true); }); + this.conditionFormGroup.get('expression.filters').valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((filters) => { + this.checkIsNoData(filters); + }); + this.conditionFormGroup.get('expression.type').valueChanges.pipe( takeUntilDestroyed() ).subscribe((type) => { @@ -185,6 +196,25 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent) { + this.isNoData = this.hasNoData(filters); + if (this.isNoData && this.conditionFormGroup.get('type').value !== AlarmRuleConditionType.SIMPLE) { + this.conditionFormGroup.get('type').patchValue(AlarmRuleConditionType.SIMPLE); + } + } + + private hasNoData(data: Array) { + const search = (filter) => { + if (!filter) return false; + if (Array.isArray(filter)) return filter.some(search); + if (typeof filter !== 'object') return false; + if (filter.type === 'NO_DATA') return true; + if (filter.predicates?.length) return filter.predicates.some(search); + return false; + }; + return search(data); + }; + private updateValidators(type: AlarmRuleConditionType, emitEvent = false) { switch (type) { case AlarmRuleConditionType.DURATION: diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts index 941ce326e1..d2c9389de1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts @@ -24,10 +24,13 @@ import { DialogComponent } from '@app/shared/components/dialog.component'; import { ComplexOperation, complexOperationTranslationMap, - EntityKeyValueType, - FilterPredicateType + EntityKeyValueType } from '@shared/models/query/query.models'; -import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; +import { + AlarmRuleFilterPredicate, + AlarmRuleFilterPredicateType, + ComplexAlarmRuleFilterPredicate +} from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; export interface AlarmRuleComplexFilterPredicateDialogData { @@ -81,7 +84,7 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends save(): void { const predicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate; - predicate.type = FilterPredicateType.COMPLEX; + predicate.type = AlarmRuleFilterPredicateType.COMPLEX; this.dialogRef.close(predicate); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts index 6961524bdd..0858fd90ae 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts @@ -78,7 +78,7 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent
{{ filterControl.value?.argument }}
-
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}
+
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts index 036083c826..3300a1fceb 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -49,7 +49,7 @@ import { FormControlsFrom } from "@shared/models/tenant.model"; } ] }) -export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit { +export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { @Input() arguments: Record; @@ -110,6 +110,20 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces ).subscribe(value => this.updateValueModeValidators(value)); } + ngOnChanges(changes: SimpleChanges) { + if (changes.argumentInUse) { + const argumentInUseChanges = changes.argumentInUse; + if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) { + if (this.dynamicModeControl.value) { + if (this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) { + this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true}); + this.filterPredicateValueFormGroup.updateValueAndValidity(); + } + } + } + } + } + setDisabledState(isDisabled: boolean): void { if (isDisabled) { this.filterPredicateValueFormGroup.disable({emitEvent: false}); @@ -125,6 +139,12 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces if (isDynamicMode) { this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable(); + setTimeout(()=> { + if (this.filterPredicateValueFormGroup.get('dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueFormGroup.get('dynamicValueArgument').value) { + this.filterPredicateValueFormGroup.get('dynamicValueArgument').setErrors({argumentInUse: true}); + this.filterPredicateValueFormGroup.updateValueAndValidity(); + } + }, 0); } else { this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false}); this.filterPredicateValueFormGroup.get('staticValue').enable(); @@ -146,7 +166,7 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces writeValue(predicateValue: AlarmRuleValue): void { this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); - this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length, {emitEvent: false}); + this.dynamicModeControl.patchValue(!!predicateValue?.dynamicValueArgument?.length, {emitEvent: false}); } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html index d73c222f9d..b297117b5d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html @@ -27,9 +27,11 @@ - - {{ 'alarm-rule.ignore-case' | translate }} - + @if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) { + + {{ 'alarm-rule.ignore-case' | translate }} + + }
} @case (filterPredicateType.NUMERIC) { @@ -68,7 +70,14 @@ } } - @if (type !== filterPredicateType.COMPLEX) { + @if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) { + + + } @else if (type !== filterPredicateType.COMPLEX) { { }; @@ -132,11 +134,45 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, writeValue(predicate: AlarmRuleFilterPredicate): void { this.type = predicate.type; - this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); + if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) { + this.type = AlarmRuleFilterPredicateType[this.valueType]; + this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false}); + } else { + this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); + } } private updateModel() { - this.propagateChange({type: this.type, ...this.filterPredicateFormGroup.value}); + const predicate = this.filterPredicateFormGroup.value; + 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}); + } } public openComplexFilterDialog() { 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 498b2a0a81..f824fdc876 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 @@ -16,25 +16,27 @@ import { Component, Input } from '@angular/core'; import { - booleanOperationTranslationMap, ComplexOperation, complexOperationTranslationMap, - EntityKeyValueType, - FilterPredicateType, - numericOperationTranslationMap, - stringOperationTranslationMap + EntityKeyValueType } from '@shared/models/query/query.models'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; import { + alarmRuleBooleanOperationTranslationMap, AlarmRuleExpression, AlarmRuleExpressionType, AlarmRuleFilter, AlarmRuleFilterPredicate, + AlarmRuleFilterPredicateType, + alarmRuleNumericOperationTranslationMap, + AlarmRuleStringOperation, + alarmRuleStringOperationTranslationMap, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { coerceBoolean } from "@shared/decorators/coercion"; +import { timeUnitTranslationMap } from "@shared/models/time/time.models"; @Component({ selector: 'tb-alarm-rule-filter-text', @@ -137,21 +139,21 @@ export class AlarmRuleFilterTextComponent { const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND); const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => { - if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { + if (keyFilterPredicate.type === AlarmRuleFilterPredicateType.COMPLEX) { const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate; const complexOperation = complexPredicate.operation ?? ComplexOperation.AND; return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation); } else { let operation: string; let value: string; - const val = keyFilterPredicate.value; + const val = keyFilterPredicate.type === AlarmRuleFilterPredicateType.NO_DATA ? keyFilterPredicate.duration : keyFilterPredicate.value; const dynamicValue = val?.dynamicValueArgument?.length; if (dynamicValue) { value = '' + val?.dynamicValueArgument + ''; } switch (keyFilterPredicate.type) { - case FilterPredicateType.STRING: - operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation)); + case AlarmRuleFilterPredicateType.STRING: + operation = translate.instant(alarmRuleStringOperationTranslationMap.get(keyFilterPredicate.operation)); if (keyFilterPredicate.ignoreCase) { operation += ' ' + translate.instant('filter.ignore-case'); } @@ -159,8 +161,8 @@ export class AlarmRuleFilterTextComponent { value = `'${keyFilterPredicate.value.staticValue}'`; } break; - case FilterPredicateType.NUMERIC: - operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation)); + case AlarmRuleFilterPredicateType.NUMERIC: + operation = translate.instant(alarmRuleNumericOperationTranslationMap.get(keyFilterPredicate.operation)); if (!dynamicValue) { if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) { value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm'); @@ -169,12 +171,18 @@ export class AlarmRuleFilterTextComponent { } } break; - case FilterPredicateType.BOOLEAN: - operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation)); + case AlarmRuleFilterPredicateType.BOOLEAN: + operation = translate.instant(alarmRuleBooleanOperationTranslationMap.get(keyFilterPredicate.operation)); if (!dynamicValue) { value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false'); } break; + case AlarmRuleFilterPredicateType.NO_DATA: + operation = translate.instant(alarmRuleStringOperationTranslationMap.get(AlarmRuleStringOperation.NO_DATA)); + if (!dynamicValue) { + value = keyFilterPredicate.duration.staticValue + ' ' + translate.instant(timeUnitTranslationMap.get(keyFilterPredicate.unit)).toLowerCase(); + } + break; } if (!dynamicValue) { value = `${value}`; 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 c9b549d78a..b92bf8bea6 100644 --- a/ui-ngx/src/app/shared/models/alarm-rule.models.ts +++ b/ui-ngx/src/app/shared/models/alarm-rule.models.ts @@ -118,7 +118,8 @@ export interface AlarmRulePredicateInfo { export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate | NumericAlarmRuleFilterPredicate | BooleanAlarmRuleFilterPredicate | - ComplexAlarmRuleFilterPredicate; + ComplexAlarmRuleFilterPredicate | + NoDataAlarmRuleFilterPredicate; export interface AlarmRuleValue { dynamicValueArgument?: string; @@ -126,26 +127,33 @@ export interface AlarmRuleValue { } export interface StringAlarmRuleFilterPredicate { - type: FilterPredicateType.STRING; - operation: StringOperation; + type: AlarmRuleFilterPredicateType.STRING; + operation: AlarmRuleStringOperation; value: AlarmRuleValue; ignoreCase: boolean; } export interface NumericAlarmRuleFilterPredicate { - type: FilterPredicateType.NUMERIC; - operation: NumericOperation; + type: AlarmRuleFilterPredicateType.NUMERIC; + operation: AlarmRuleNumericOperation; value: AlarmRuleValue; } export interface BooleanAlarmRuleFilterPredicate { - type: FilterPredicateType.BOOLEAN; - operation: BooleanOperation; + type: AlarmRuleFilterPredicateType.BOOLEAN; + operation: AlarmRuleBooleanOperation; value: AlarmRuleValue; } +export interface NoDataAlarmRuleFilterPredicate { + type: AlarmRuleFilterPredicateType.NO_DATA; + unit: TimeUnit, + operation: AlarmRuleStringOperation.NO_DATA | AlarmRuleNumericOperation.NO_DATA | AlarmRuleBooleanOperation.NO_DATA; + duration: AlarmRuleValue; +} + export interface BaseComplexFilterPredicate { - type: FilterPredicateType.COMPLEX; + type: AlarmRuleFilterPredicateType.COMPLEX; operation: ComplexOperation; predicates: Array; } @@ -157,3 +165,73 @@ export interface AlarmRuleFilterConfig { entityType?: EntityType; entities?: Array; } + +export enum AlarmRuleFilterPredicateType { + STRING = 'STRING', + NUMERIC = 'NUMERIC', + BOOLEAN = 'BOOLEAN', + COMPLEX = 'COMPLEX', + NO_DATA = 'NO_DATA' +} + +export enum AlarmRuleStringOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + NO_DATA = 'NO_DATA', + STARTS_WITH = 'STARTS_WITH', + ENDS_WITH = 'ENDS_WITH', + CONTAINS = 'CONTAINS', + NOT_CONTAINS = 'NOT_CONTAINS', + IN = 'IN', + NOT_IN = 'NOT_IN', +} + +export const alarmRuleStringOperationTranslationMap = new Map( + [ + [AlarmRuleStringOperation.EQUAL, 'filter.operation.equal'], + [AlarmRuleStringOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [AlarmRuleStringOperation.STARTS_WITH, 'filter.operation.starts-with'], + [AlarmRuleStringOperation.ENDS_WITH, 'filter.operation.ends-with'], + [AlarmRuleStringOperation.CONTAINS, 'filter.operation.contains'], + [AlarmRuleStringOperation.NOT_CONTAINS, 'filter.operation.not-contains'], + [AlarmRuleStringOperation.IN, 'filter.operation.in'], + [AlarmRuleStringOperation.NOT_IN, 'filter.operation.not-in'], + [AlarmRuleStringOperation.NO_DATA, 'alarm-rule.missing-for'] + ] +); + +export enum AlarmRuleNumericOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + NO_DATA = 'NO_DATA', + GREATER = 'GREATER', + LESS = 'LESS', + GREATER_OR_EQUAL = 'GREATER_OR_EQUAL', + LESS_OR_EQUAL = 'LESS_OR_EQUAL' +} + +export const alarmRuleNumericOperationTranslationMap = new Map( + [ + [AlarmRuleNumericOperation.EQUAL, 'filter.operation.equal'], + [AlarmRuleNumericOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [AlarmRuleNumericOperation.GREATER, 'filter.operation.greater'], + [AlarmRuleNumericOperation.LESS, 'filter.operation.less'], + [AlarmRuleNumericOperation.GREATER_OR_EQUAL, 'filter.operation.greater-or-equal'], + [AlarmRuleNumericOperation.LESS_OR_EQUAL, 'filter.operation.less-or-equal'], + [AlarmRuleNumericOperation.NO_DATA, 'alarm-rule.missing-for'] + ] +); + +export enum AlarmRuleBooleanOperation { + EQUAL = 'EQUAL', + NOT_EQUAL = 'NOT_EQUAL', + NO_DATA = 'NO_DATA', +} + +export const alarmRuleBooleanOperationTranslationMap = new Map( + [ + [AlarmRuleBooleanOperation.EQUAL, 'filter.operation.equal'], + [AlarmRuleBooleanOperation.NOT_EQUAL, 'filter.operation.not-equal'], + [AlarmRuleBooleanOperation.NO_DATA, 'alarm-rule.missing-for'] + ] +); 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 538b3a8352..1899802715 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1466,6 +1466,7 @@ "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", "edit-alarm-rule-condition": "Edit alarm rule condition", "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": "Please add create alarm rule", "add-create-alarm-rule": "Add create condition", @@ -1497,7 +1498,12 @@ "no-alarm-rule-types-matching": "No alarm rule types matching '{{entitySubtype}}' were found.", "alarm-rule-type-list-empty": "No alarm rule types selected.", "alarm-rule-type-list": "Alarm rule type list", - "alarm-rule-entity-list": "Entity list" + "alarm-rule-entity-list": "Entity list", + "missing-for": "missing for", + "time-unit": "Unit", + "value-required": "Value is required.", + "min-value": "Value must be 0 or greater.", + "argument-in-use": "Argument is used as general argument." }, "ai-models": { "ai-models": "AI models", From c5050890ba4620bda8c7b3c6d70f69732ad0342b Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 28 Nov 2025 12:55:56 +0200 Subject: [PATCH 07/10] Update VC for alarm rules CFs --- .../impl/DefaultEntityExportService.java | 9 + .../impl/BaseEntityImportService.java | 9 + .../server/controller/AbstractWebTest.java | 31 ++- .../CalculatedFieldControllerTest.java | 20 -- .../service/sync/vc/VersionControlTest.java | 187 +++++++++++++----- .../common/data/alarm/rule/AlarmRule.java | 4 + .../alarm/rule/condition/AlarmCondition.java | 2 +- .../TbelAlarmConditionExpression.java | 4 + 8 files changed, 187 insertions(+), 79 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index 5d634c4178..70e90b2636 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.sync.ie.exporting.impl; +import org.apache.commons.lang3.tuple.Pair; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; @@ -24,6 +25,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -170,6 +172,13 @@ public class DefaultEntityExportService { + if (rule.getDashboardId() != null) { + rule.setDashboardId(getExternalIdOrElseInternal(ctx, rule.getDashboardId())); + } + }); + } }); return calculatedFields; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index c95fffc205..61aca6eb8f 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -22,6 +22,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.HasVersion; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -338,6 +340,13 @@ public abstract class BaseEntityImportService { + if (rule.getDashboardId() != null) { + rule.setDashboardId(idProvider.getInternalId(rule.getDashboardId(), ctx.isFinalImportAttempt())); + } + }); + } }).toList(); for (CalculatedField existingField : existing) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 51d77bd134..80e79280df 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -84,6 +84,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.StringUtils; @@ -93,6 +94,7 @@ import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; @@ -1207,8 +1209,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { CalculatedFieldState calculatedFieldState = statesMap.get(cfId); - boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() - < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() < + System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); return isReady; }); @@ -1399,7 +1401,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected List findJobs(List types, List entities) throws Exception { return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + - "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", + "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } @@ -1413,12 +1415,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void postTelemetry(EntityId entityId, String payload) throws Exception { doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + - "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); } protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + - "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); } protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { @@ -1427,7 +1429,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected PageData getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + - (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + } + + protected PageData getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", + new TypeReference>() {}, pageLink); + } + + protected List getCalculatedFields(CalculatedFieldType type, + EntityType entityType, + List entities, + List names) throws Exception { + return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" + + (entityType != null ? "entityType=" + entityType + "&" : "") + + (entities != null ? "entities=" + String.join(",", + entities.stream().map(UUID::toString).toList()) + "&" : "") + + (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), + new TypeReference>() {}, new PageLink(10)).getData(); } protected PageData getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 635fc747e5..33f6bfa250 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.type.TypeReference; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -56,9 +55,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; @@ -295,23 +292,6 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(names.getData()).containsOnly(deviceCalculatedField.getName()); } - private PageData getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { - return doGetTypedWithPageLink("/api/calculatedFields/names?type=" + type + "&", - new TypeReference>() {}, pageLink); - } - - private List getCalculatedFields(CalculatedFieldType type, - EntityType entityType, - List entities, - List names) throws Exception { - return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" + - (entityType != null ? "entityType=" + entityType + "&" : "") + - (entities != null ? "entities=" + String.join(",", - entities.stream().map(UUID::toString).toList()) + "&" : "") + - (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), - new TypeReference>() {}, new PageLink(10)).getData(); - } - @Test public void testDeleteCalculatedField() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index c6ad3c12d3..cc55e09fd5 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.collect.Streams; +import org.apache.commons.lang3.tuple.Pair; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -45,10 +46,15 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.SimpleAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; @@ -306,6 +312,54 @@ public class VersionControlTest extends AbstractControllerTest { checkImportedOtaPackageData(software, importedSoftwareOta); } + @Test + public void testDeviceVc_withAlarmRules_betweenTenants() throws Exception { + DeviceProfile deviceProfile = createDeviceProfile(null, null, "Device profile of tenant 1"); + Dashboard dashboard = createDashboard(null, "Mobile dashboard"); + createAlarmRule(deviceProfile.getId(), "Profile alarm rule", dashboard.getId()); + Device device = createDevice(deviceProfile.getId(), "Device of tenant 1", "test1"); + createAlarmRule(device.getId(), "Device alarm rule", dashboard.getId()); + String version = createVersion("devices, profiles and dashboards", EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD); + + loginTenant2(); + Map result = loadVersion(version, config -> { + config.setLoadCredentials(false); + }, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.DASHBOARD); + assertThat(result.get(EntityType.DEVICE).getCreated()).isEqualTo(1); + assertThat(result.get(EntityType.DEVICE_PROFILE).getCreated()).isEqualTo(1); + assertThat(result.get(EntityType.DASHBOARD).getCreated()).isEqualTo(1); + + Device importedDevice = findDevice(device.getName()); + checkImportedEntity(tenantId1, device, tenantId2, importedDevice); + checkImportedDeviceData(device, importedDevice); + + DeviceProfile importedDeviceProfile = findDeviceProfile(deviceProfile.getName()); + checkImportedEntity(tenantId1, deviceProfile, tenantId2, importedDeviceProfile); + checkImportedDeviceProfileData(deviceProfile, importedDeviceProfile); + assertThat(importedDevice.getDeviceProfileId()).isEqualTo(importedDeviceProfile.getId()); + + Dashboard importedDashboard = findDashboard(dashboard.getName()); + checkImportedEntity(tenantId1, dashboard, tenantId2, importedDashboard); + checkImportedDashboardData(dashboard, importedDashboard); + + getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE, + List.of(importedDeviceProfile.getUuidId()), null).forEach(alarmRuleCf -> { + assertThat(alarmRuleCf.getName()).isEqualTo("Profile alarm rule"); + AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration(); + config.getAllRules().map(Pair::getValue).forEach(alarmRule -> { + assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId()); + }); + }); + getCalculatedFields(CalculatedFieldType.ALARM, EntityType.DEVICE_PROFILE, + List.of(importedDevice.getUuidId()), null).forEach(alarmRuleCf -> { + assertThat(alarmRuleCf.getName()).isEqualTo("Device alarm rule"); + AlarmCalculatedFieldConfiguration config = (AlarmCalculatedFieldConfiguration) alarmRuleCf.getConfiguration(); + config.getAllRules().map(Pair::getValue).forEach(alarmRule -> { + assertThat(alarmRule.getDashboardId()).isEqualTo(importedDashboard.getId()); + }); + }); + } + @Test public void testDashboardVc_betweenTenants() throws Exception { Dashboard dashboard = createDashboard(null, "Dashboard of tenant 1"); @@ -368,42 +422,42 @@ public class VersionControlTest extends AbstractControllerTest { String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c"; String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa"; String entityAliases = "{\n" + - "\"" + aliasId + "\": {\n" + - "\"alias\": \"assets\",\n" + - "\"filter\": {\n" + - " \"entityList\": [\n" + - " \"" + asset1.getId() + "\",\n" + - " \"" + asset2.getId() + "\",\n" + - " \"" + tenantId1.getId() + "\",\n" + - " \"" + existingDeviceProfile.getId() + "\",\n" + - " \"" + unknownUuid + "\"\n" + - " ],\n" + - " \"id\":\"" + asset1.getId() + "\",\n" + - " \"resolveMultiple\": true\n" + - "},\n" + - "\"id\": \"" + aliasId + "\"\n" + - "}\n" + - "}"; + "\"" + aliasId + "\": {\n" + + "\"alias\": \"assets\",\n" + + "\"filter\": {\n" + + " \"entityList\": [\n" + + " \"" + asset1.getId() + "\",\n" + + " \"" + asset2.getId() + "\",\n" + + " \"" + tenantId1.getId() + "\",\n" + + " \"" + existingDeviceProfile.getId() + "\",\n" + + " \"" + unknownUuid + "\"\n" + + " ],\n" + + " \"id\":\"" + asset1.getId() + "\",\n" + + " \"resolveMultiple\": true\n" + + "},\n" + + "\"id\": \"" + aliasId + "\"\n" + + "}\n" + + "}"; String widgetId = "ea8f34a0-264a-f11f-cde3-05201bb4ff4b"; String actionId = "4a8e6efa-3e68-fa59-7feb-d83366130cae"; String widgets = "{\n" + - " \"" + widgetId + "\": {\n" + - " \"config\": {\n" + - " \"actions\": {\n" + - " \"rowClick\": [\n" + - " {\n" + - " \"name\": \"go to dashboard\",\n" + - " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + - " \"id\": \"" + actionId + "\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " \"row\": 0,\n" + - " \"col\": 0,\n" + - " \"id\": \"" + widgetId + "\"\n" + - " }\n" + - "}"; + " \"" + widgetId + "\": {\n" + + " \"config\": {\n" + + " \"actions\": {\n" + + " \"rowClick\": [\n" + + " {\n" + + " \"name\": \"go to dashboard\",\n" + + " \"targetDashboardId\": \"" + otherDashboard.getId() + "\",\n" + + " \"id\": \"" + actionId + "\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"row\": 0,\n" + + " \"col\": 0,\n" + + " \"id\": \"" + widgetId + "\"\n" + + " }\n" + + "}"; ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); @@ -499,11 +553,12 @@ public class VersionControlTest extends AbstractControllerTest { generatorNodeConfig.setMsgCount(1); generatorNodeConfig.setScriptLang(ScriptLanguage.JS); UUID someUuid = UUID.randomUUID(); - generatorNodeConfig.setJsScript("var msg = { temp: 42, humidity: 77 };\n" + - "var metadata = { data: 40 };\n" + - "var msgType = \"POST_TELEMETRY_REQUEST\";\n" + - "var someUuid = \"" + someUuid + "\";\n" + - "return { msg: msg, metadata: metadata, msgType: msgType };"); + generatorNodeConfig.setJsScript(""" + var msg = { temp: 42, humidity: 77 }; + var metadata = { data: 40 }; + var msgType = "POST_TELEMETRY_REQUEST"; + var someUuid = "%s"; + return { msg: msg, metadata: metadata, msgType: msgType };""".formatted(someUuid)); generatorNode.setConfiguration(JacksonUtil.valueToTree(generatorNodeConfig)); nodes.add(generatorNode); metaData.setNodes(nodes); @@ -1018,20 +1073,21 @@ public class VersionControlTest extends AbstractControllerTest { protected Dashboard createDashboard(CustomerId customerId, String name, AssetId assetForEntityAlias) { Dashboard dashboard = createDashboard(customerId, name); - String entityAliases = "{\n" + - "\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + - "\t\t\"alias\": \"assets\",\n" + - "\t\t\"filter\": {\n" + - "\t\t\t\"entityList\": [\n" + - "\t\t\t\t\"" + assetForEntityAlias.getId().toString() + "\"\n" + - "\t\t\t],\n" + - "\t\t\t\"entityType\": \"ASSET\",\n" + - "\t\t\t\"resolveMultiple\": true,\n" + - "\t\t\t\"type\": \"entityList\"\n" + - "\t\t},\n" + - "\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + - "\t}\n" + - "}"; + String entityAliases = """ + { + "23c4185d-1497-9457-30b2-6d91e69a5b2c": { + "alias": "assets", + "filter": { + "entityList": [ + "%s" + ], + "entityType": "ASSET", + "resolveMultiple": true, + "type": "entityList" + }, + "id": "23c4185d-1497-9457-30b2-6d91e69a5b2c" + } + }""".formatted(assetForEntityAlias.getId().toString()); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("description", new TextNode("hallo")); @@ -1134,6 +1190,33 @@ public class VersionControlTest extends AbstractControllerTest { return doPost("/api/calculatedField", calculatedField, CalculatedField.class); } + private CalculatedField createAlarmRule(EntityId entityId, String alarmType, DashboardId mobileDashboardId) { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setName(alarmType); + calculatedField.setType(CalculatedFieldType.ALARM); + AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); + configuration.setArguments(arguments); + SimpleAlarmCondition createCondition = new SimpleAlarmCondition(); + createCondition.setExpression(new TbelAlarmConditionExpression("return temperature >= 50;")); + configuration.setCreateRules(Map.of( + AlarmSeverity.CRITICAL, new AlarmRule(createCondition, "", mobileDashboardId) + )); + SimpleAlarmCondition clearCondition = new SimpleAlarmCondition(); + clearCondition.setExpression(new TbelAlarmConditionExpression("return temperature < 50;")); + configuration.setClearRule(new AlarmRule(clearCondition, "", mobileDashboardId)); + calculatedField.setConfiguration(configuration); + calculatedField.setDebugSettings(DebugSettings.all()); + return saveCalculatedField(calculatedField); + } + private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java index 23dbc8fede..7dcf7ffe66 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java @@ -18,11 +18,15 @@ package org.thingsboard.server.common.data.alarm.rule; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmCondition; import org.thingsboard.server.common.data.id.DashboardId; @Data +@AllArgsConstructor +@NoArgsConstructor public class AlarmRule { @Valid diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java index b4e1446798..073d9347fa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java @@ -22,9 +22,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonTypeInfo; import jakarta.validation.Valid; import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.NoArgsConstructor; -import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule; import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AnyTimeSchedule; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java index 50f73e887b..2562e19be4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/TbelAlarmConditionExpression.java @@ -16,9 +16,13 @@ package org.thingsboard.server.common.data.alarm.rule.condition.expression; import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@AllArgsConstructor +@NoArgsConstructor public class TbelAlarmConditionExpression implements AlarmConditionExpression { @NotBlank From 87345e7161111c86ef3dab5f99f5441df6802645 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 28 Nov 2025 15:33:11 +0200 Subject: [PATCH 08/10] Fix EdqsEntityQueryControllerTest init --- .../server/controller/EdqsEntityQueryControllerTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 15ad57b62e..dca25a83a1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -20,7 +20,6 @@ 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.MockitoBean; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; @@ -33,7 +32,6 @@ import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.edqs.util.EdqsRocksDb; import org.thingsboard.server.queue.discovery.DiscoveryService; import java.util.concurrent.TimeUnit; @@ -59,9 +57,6 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { @Autowired private DiscoveryService discoveryService; - @MockitoBean // so that we don't do backup for tests - private EdqsRocksDb edqsRocksDb; - @Before public void before() { await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.getState().isApiEnabled()); From ebffe261084e1f5e14083a3b81fad3439ce705ee Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 1 Dec 2025 19:24:41 +0200 Subject: [PATCH 09/10] UI: Bug-fixes and add validation for alarm rules --- ui-ngx/src/app/core/services/menu.models.ts | 2 +- .../alarm-rule-details-dialog.component.html | 4 +- .../alarm-rule-dialog.component.html | 48 +++---- .../alarm-rule-filter-config.component.html | 4 +- .../alarm-rules/alarm-rules-table-config.ts | 6 +- ...alarm-rule-condition-dialog.component.html | 47 ++++++- ...f-alarm-rule-condition-dialog.component.ts | 129 ++++++++++++++++-- .../cf-alarm-rule-condition.component.html | 11 +- .../cf-alarm-rule-condition.component.ts | 74 +++++++++- .../alarm-rules/cf-alarm-rule.component.html | 6 +- .../cf-alarm-rules-dialog.component.scss | 5 + .../cf-alarm-schedule.component.ts | 2 +- ...lex-filter-predicate-dialog.component.html | 33 +++-- ...mplex-filter-predicate-dialog.component.ts | 13 +- .../alarm-rule-filter-dialog.component.html | 4 +- .../alarm-rule-filter-dialog.component.ts | 51 +++++-- .../alarm-rule-filter-list.component.html | 32 +++-- .../alarm-rule-filter-list.component.scss | 13 +- .../alarm-rule-filter-list.component.ts | 30 ++++ ...-rule-filter-predicate-list.component.html | 16 ++- ...-rule-filter-predicate-list.component.scss | 3 +- ...ilter-predicate-no-data-value.component.ts | 9 +- ...m-rule-filter-predicate-value.component.ts | 9 +- ...alarm-rule-filter-predicate.component.html | 2 +- .../alarm-rule-filter-predicate.component.ts | 36 +++++ .../alarm-rule-filter-text.component.html | 1 + .../alarm-rule-filter-text.component.scss | 3 + .../alarm-rule-filter-text.component.ts | 2 +- ...y-aggregation-arguments-table.component.ts | 1 + .../propagate-arguments-table.component.ts | 2 +- ...d-aggregation-arguments-table.component.ts | 3 +- .../propagation-configuration.component.html | 2 +- .../simple-configuration.component.ts | 2 +- ...entity-types-version-create.component.html | 4 +- .../vc/entity-types-version.component.scss | 11 ++ .../home/pages/alarm/alarm-routing.module.ts | 2 +- .../app/shared/models/alarm-rule.models.ts | 7 + ui-ngx/src/app/shared/models/vc.models.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 50 ++++--- 39 files changed, 535 insertions(+), 146 deletions(-) diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 8909663771..555717ffc0 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -510,7 +510,7 @@ export const menuSectionMap = new Map([ MenuId.alarms, { id: MenuId.alarms, - name: 'alarm.alarms', + name: 'alarm.alarm-list', type: 'link', path: '/alarms/alarms', icon: 'mdi:alert-outline' diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html index 9e5036f6ff..97bd25836a 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html @@ -27,8 +27,10 @@ - +
+ +
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 b734757144..20bd3e1bde 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 @@ -130,30 +130,32 @@ {{ 'alarm-rule.advanced-settings' | translate }} -
- - {{ 'alarm-rule.propagate-alarm' | translate }} - +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + alarm-rule.alarm-rule-relation-types-list + + + {{key}} + close + + + + + + }
- @if (configFormGroup.get('propagate').value) { - - alarm-rule.alarm-rule-relation-types-list - - - {{key}} - close - - - - - - }
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html index a71874c13c..5deddffd87 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html @@ -66,7 +66,7 @@
-
alarm-rule.entity-type
+
alarm-rule.target-entity-type
{{ 'alarm-rule.any-type' | translate }} @@ -78,7 +78,7 @@
@if (alarmRuleFilterConfigForm.get('entityType').value) {
-
alarm-rule.alarm-rule-entity-list
+
alarm-rule.target-entities
{ this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.entity-type', '15%', + this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.target-entity-type', '15%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.entity-name', '30%', + this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.target-entity', '30%', entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%', entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), () => ({}), false)); - this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '70px', + this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '90px', entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false)); this.cellActionDescriptors.push( 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 8116431757..d67cd51a9a 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 @@ -90,6 +90,34 @@
}
+ + @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { +
+ + + {{ 'alarm-rule.filter-preview' | translate }} + + +
+ @if (specText) { + {{ specText }} + } + @if (conditionFormGroup.get('expression.filters').value?.length) { + + + } @else { + {{ 'alarm-rule.no-filter-preview' | translate }} + } +
+
+
+
+ } +
{{ 'alarm-rule.condition-settings' | translate }}
@@ -150,7 +178,7 @@
- +
@@ -166,9 +194,8 @@ @if (!readonly) { } @@ -190,6 +217,9 @@ } @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) { {{ defaultValuePatternError | translate }} } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } @@ -202,9 +232,14 @@ {{ argument }} } - - {{ 'calculated-fields.hint.argument-name-required' | translate }} - + @if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) { + + {{ 'calculated-fields.hint.argument-name-required' | translate }} + + } + @if (type === AlarmConditionType.REPEATING) { + alarm-rule.condition-repeating-value-hint + } 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 3454903b55..f565a66168 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 @@ -24,14 +24,6 @@ import { DialogComponent } from '@app/shared/components/dialog.component'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ScriptLanguage } from "@shared/models/rule-node.models"; -import { - AlarmRuleCondition, - AlarmRuleConditionType, - AlarmRuleConditionTypeTranslationMap, - alarmRuleDefaultScript, - AlarmRuleExpressionType, - AlarmRuleFilter -} from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument, getCalculatedFieldArgumentsEditorCompleter, @@ -39,8 +31,18 @@ import { } from "@shared/models/calculated-field.models"; import { TbEditorCompleter } from "@shared/models/ace/completion.models"; import { AceHighlightRules } from "@shared/models/ace/ace.models"; -import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; +import { ComplexOperation } from "@shared/models/query/query.models"; import { Observable } from "rxjs"; +import { TranslateService } from "@ngx-translate/core"; +import { + AlarmRuleCondition, + AlarmRuleConditionType, + AlarmRuleConditionTypeTranslationMap, + alarmRuleDefaultScript, + AlarmRuleExpressionType, + AlarmRuleFilter, + filterOperationTranslationMap +} from "@shared/models/alarm-rule.models"; export interface CfAlarmRuleConditionDialogData { readonly: boolean; @@ -97,7 +99,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent(false); ComplexOperation = ComplexOperation; - complexOperationTranslationMap = complexOperationTranslationMap; + complexOperationTranslationMap = filterOperationTranslationMap; + + specText = ''; + + filtersValid: boolean = false; functionArgs: Array; argumentsEditorCompleter: TbEditorCompleter; @@ -112,7 +118,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent, - private fb: FormBuilder) { + private fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; @@ -131,28 +138,37 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent { - this.updateValidators(type, true); + this.updateValidators(type); }); + this.conditionFormGroup.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe((value) => { + this.updateSpecText(value.type); + }) + this.conditionFormGroup.get('expression.filters').valueChanges.pipe( takeUntilDestroyed() ).subscribe((filters) => { + this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList); this.checkIsNoData(filters); }); @@ -175,6 +191,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent 0) { + this.specText = this.specText + ':'; + } + } + cancel(): void { this.dialogRef.close(null); } 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 0b722b4ec1..638075849f 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 @@ -21,7 +21,7 @@
{{ 'alarm-rule.schedule-title' | translate }}
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 543b0a86dc..6f17aa8b59 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 @@ -14,8 +14,9 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; import { + AbstractControl, ControlValueAccessor, FormBuilder, NG_VALIDATORS, @@ -68,7 +69,7 @@ import { Observable } from "rxjs"; } ] }) -export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator { +export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges { @Input() @coerceBoolean() @@ -96,11 +97,15 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali specText = ''; + filtersArgumentsValid: boolean = true; + schedulerArgumentsValid: boolean = true; + scheduleText = ''; private modelValue: AlarmRuleCondition; private propagateChange = (v: any) => { }; + private onValidatorChange = () => { }; constructor(private dialog: MatDialog, private fb: FormBuilder, @@ -115,6 +120,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali registerOnTouched(fn: any): void { } + registerOnValidatorChange(fn: () => void): void { + this.onValidatorChange = fn; + } + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; if (this.disabled) { @@ -127,14 +136,68 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali writeValue(value: AlarmRuleCondition): void { this.modelValue = value; this.updateConditionInfo(); + if (value) { + this.onValidatorChange(); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.arguments) { + if (changes.arguments && !changes.arguments.firstChange && this.modelValue) { + this.onValidatorChange(); + } + } + } + + private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean { + const arg = obj?.schedule?.dynamicValueArgument; + return !arg || validArguments.includes(arg); + } + + private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean { + const validSet = new Set(validArguments); + 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)) { + return false; + } + } + } + return true; } public conditionSet() { return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); } - public validate(): ValidationErrors | null { - return this.conditionSet() ? null : { + public validate(control: AbstractControl): ValidationErrors | null { + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments)); + this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); + this.onValidatorChange = () => { + control.updateValueAndValidity({ emitEvent: true }); + }; + return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : { alarmRuleCondition: { valid: false, } @@ -226,6 +289,9 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali private updateModel() { this.updateConditionInfo(); this.propagateChange(this.modelValue); + if (this.modelValue) { + this.onValidatorChange(); + } } public openScheduleDialog($event: Event) { 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 38e0bd8023..a9f4ad61a5 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 @@ -20,11 +20,11 @@ @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
-
+
alarm-rule.alarm-rule-additional-info
- +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss index b66573f417..6253aa9a4b 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -20,6 +20,11 @@ display: grid; grid-template-rows: min-content minmax(auto, 1fr) min-content; } + + .spec-text { + font-size: 14px; + color: rgba(0, 0, 0, 0.54); + } } .tbel-script-lang-chip { line-height: 20px; diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts index b1c7a4da7f..a4572629e0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts @@ -171,7 +171,7 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator if (value) { this.modelValue = value; if (this.modelValue.dynamicValueArgument) { - this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false}); + this.alarmScheduleForm.get('dynamicValueArgument').patchValue(Object.keys(this.arguments).includes(this.modelValue.dynamicValueArgument) ? this.modelValue.dynamicValueArgument : null, {emitEvent: false}); } else { switch (this.modelValue.staticValue.type) { case AlarmRuleScheduleType.SPECIFIC_TIME: diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html index 75bce37a80..c0c5a72d77 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -25,20 +25,25 @@
- - filter.operation.operation - - - {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} - - - - - +
+
+
+ {{ 'alarm-rule.filters' | translate }} +
+ + {{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }} + {{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }} + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts index 0858fd90ae..3f86bdbe04 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts @@ -21,18 +21,18 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; - import { - ComplexOperation, - complexOperationTranslationMap, - EntityKeyValueType, - entityKeyValueTypesMap - } from '@shared/models/query/query.models'; + import { ComplexOperation, EntityKeyValueType, entityKeyValueTypesMap } from '@shared/models/query/query.models'; import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; - import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; + import { + AlarmRuleFilter, + AlarmRuleFilterPredicate, + filterOperationTranslationMap + } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { FormControlsFrom } from "@shared/models/tenant.model"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + import { isDefinedAndNotNull } from "@core/utils"; export interface AlarmRuleFilterDialogData { filter: AlarmRuleFilter; @@ -57,7 +57,9 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent { + this.predicatesValid = this.isPredicateArgumentsValid(predicates); + }); + this.filterFormGroup.get('valueType').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((valueType: EntityKeyValueType) => { @@ -106,6 +116,31 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent
-
{{ filterControl.value?.argument }}
-
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
-
+
{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}
+ @@ -62,14 +69,15 @@ }
} - - filter.no-key-filters - + @if (!filtersFormArray.length) { + + alarm-rule.no-filter + + + }
- -