From 3922788c837a9c3d938814d048867b7c3a4b2ad3 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 24 Nov 2025 16:24:25 +0200 Subject: [PATCH] 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