Browse Source

Handling of no-data alarm rule predicate

pull/14440/head
Viacheslav Klimov 10 months ago
parent
commit
3922788c83
  1. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  2. 45
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  3. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java
  4. 27
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  5. 56
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  6. 10
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java
  7. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NoDataFilterPredicate.java
  8. 4
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java

1
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(); boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation();
if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) {
// TODO: handle reeaval for no-data conditions!
if (requiresScheduledReevaluation) { if (requiresScheduledReevaluation) {
long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval()); long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(systemContext.getAlarmRulesReevaluationInterval());
if (now - lastReevaluationTs >= reevaluationIntervalMillis) { if (now - lastReevaluationTs >= reevaluationIntervalMillis) {

45
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.equalsAny;
import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithoutQuotes; 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.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.NOT_YET_TRUE;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.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); state.setDurationCheckFuture(future);
} }
} }
return evalResult; return evalResult.withCause(NEW_EVENT);
} else { } else {
return state.reeval(System.currentTimeMillis(), ctx); return state.reeval(System.currentTimeMillis(), ctx).withCause(SCHEDULED_REEVALUATION);
} }
}, ctx); }, ctx);
return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() return Futures.immediateFuture(AlarmCalculatedFieldResult.builder()
@ -248,12 +250,12 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
private TbAlarmResult createOrClearAlarms(Function<AlarmRuleState, AlarmEvalResult> evalFunction, private TbAlarmResult createOrClearAlarms(Function<AlarmRuleState, AlarmEvalResult> evalFunction,
CalculatedFieldCtx ctx) { CalculatedFieldCtx ctx) {
TbAlarmResult result = null; AlarmEvalResult evalResult = null;
AlarmRuleState resultState = null; AlarmRuleState resultState = null;
AlarmRuleState.StateInfo resultStateInfo = null; AlarmRuleState.StateInfo resultStateInfo = null;
for (AlarmRuleState state : createRuleStates.values()) { 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); log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult);
if (evalResult.getStatus() == TRUE) { if (evalResult.getStatus() == TRUE) {
resultState = state; resultState = state;
@ -263,13 +265,14 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
} }
} }
TbAlarmResult result = null;
if (resultState != null) { if (resultState != null) {
result = calculateAlarmResult(resultState, ctx); result = calculateAlarmResult(resultState, evalResult, ctx);
resultStateInfo = resultState.getStateInfo(); resultStateInfo = resultState.getStateInfo();
log.debug("Alarm result for state {}: {}", resultState, result); log.debug("Alarm result for state {}: {}", resultState, result);
clearState(clearRuleState); clearState(clearRuleState);
} else if (currentAlarm != null && clearRuleState != null) { } 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); log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult);
if (evalResult.getStatus() == TRUE) { if (evalResult.getStatus() == TRUE) {
resultStateInfo = clearRuleState.getStateInfo(); 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(); AlarmSeverity severity = ruleState.getSeverity();
if (currentAlarm != null) { if (currentAlarm != null) {
currentAlarm.setEndTs(System.currentTimeMillis());
AlarmSeverity oldSeverity = currentAlarm.getSeverity(); AlarmSeverity oldSeverity = currentAlarm.getSeverity();
// Skip update if severity is decreased. if (severity.ordinal() > oldSeverity.ordinal()) {
if (severity.ordinal() <= oldSeverity.ordinal()) { log.trace("Skipping alarm update for result state {} for eval result {} because severity is decreased", ruleState, evalResult);
currentAlarm.setDetails(createDetails(ruleState)); return null;
currentAlarm.setSeverity(severity); }
AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); if (severity.ordinal() == oldSeverity.ordinal() && evalResult.getCause() == SCHEDULED_REEVALUATION) {
currentAlarm = result.getAlarm(); log.trace("Skipping alarm update for result state {} for eval result {}", ruleState, evalResult);
return TbAlarmResult.fromAlarmResult(result);
} else {
return null; 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 { } else {
var newAlarm = new Alarm(); var newAlarm = new Alarm();
newAlarm.setType(alarmType); newAlarm.setType(alarmType);
@ -339,7 +346,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
newAlarm.setSeverity(severity); newAlarm.setSeverity(severity);
long startTs = latestTimestamp; long startTs = latestTimestamp;
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
if (startTs == 0L || startTs > currentTime) { if (startTs <= 0L || startTs > currentTime) {
startTs = currentTime; startTs = currentTime;
} }
newAlarm.setStartTs(startTs); newAlarm.setStartTs(startTs);
@ -518,7 +525,9 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
long passedMs = System.currentTimeMillis() - argument.getTs(); long passedMs = System.currentTimeMillis() - argument.getTs();
long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue); long duration = resolveValue(predicate.getDuration(), KvUtil::getLongValue);
if (duration > 0) { 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 { } else {
return false; return false;
} }

10
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 Status status;
private final long leftDuration; private final long leftDuration;
private final long leftEvents; private final long leftEvents;
private Cause cause;
public AlarmEvalResult(Status status) { public AlarmEvalResult(Status status) {
this(status, 0, 0); this(status, 0, 0);
@ -39,8 +40,17 @@ public class AlarmEvalResult {
return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents); return new AlarmEvalResult(Status.NOT_YET_TRUE, leftDuration, leftEvents);
} }
public AlarmEvalResult withCause(Cause cause) {
this.cause = cause;
return this;
}
public enum Status { public enum Status {
FALSE, NOT_YET_TRUE, TRUE; FALSE, NOT_YET_TRUE, TRUE;
} }
public enum Cause {
NEW_EVENT, SCHEDULED_REEVALUATION;
}
} }

27
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.Optional;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import static org.thingsboard.server.service.cf.ctx.state.alarm.AlarmEvalResult.Status.TRUE;
@Data @Data
@Slf4j @Slf4j
public class AlarmRuleState { public class AlarmRuleState {
@ -81,16 +83,20 @@ public class AlarmRuleState {
boolean active = isActive(ts); boolean active = isActive(ts);
switch (condition.getType()) { switch (condition.getType()) {
case SIMPLE, REPEATING -> { case SIMPLE, REPEATING -> {
if (this.active == null || active != this.active) { boolean activeChanged = this.active == null || active != this.active;
this.active = active; this.active = active;
if (active) { if (!active) {
return doEval(false, ctx); return AlarmEvalResult.EMPTY;
}
} }
if (active) {
return AlarmEvalResult.NOT_YET_TRUE; if ((condition.hasSchedule() && activeChanged) ||
} else { condition.getExpression().requiresScheduledReevaluation()) {
return AlarmEvalResult.FALSE; AlarmEvalResult result = doEval(false, ctx);
if (result.getStatus() == TRUE) {
return result;
} else {
return AlarmEvalResult.EMPTY;
}
} }
} }
case DURATION -> { case DURATION -> {
@ -116,7 +122,7 @@ public class AlarmRuleState {
} }
} }
} }
return AlarmEvalResult.FALSE; return AlarmEvalResult.EMPTY;
} }
public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) { public AlarmEvalResult doEval(boolean newEvent, CalculatedFieldCtx ctx) {
@ -338,6 +344,7 @@ public class AlarmRuleState {
} }
public record StateInfo(Long eventCount, Long duration) { public record StateInfo(Long eventCount, Long duration) {
static final StateInfo EMPTY = new StateInfo(null, null); static final StateInfo EMPTY = new StateInfo(null, null);
} }

56
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.ComplexOperation;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.SimpleAlarmConditionExpression; 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.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;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate.NumericFilterPredicate.NumericOperation; 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; 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<String, Argument> 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<Long>(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<Long>(criticalNoDataDuration, null))
));
criticalExpression.setFilters(List.of(criticalFilter));
Map<AlarmSeverity, Condition> 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 @Test
public void testChangeAlarmType() throws Exception { public void testChangeAlarmType() throws Exception {
Argument temperatureArgument = new Argument(); Argument temperatureArgument = new Argument();

10
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.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -55,6 +56,15 @@ public abstract class AlarmCondition {
return hasSchedule() || expression.requiresScheduledReevaluation(); 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 @JsonIgnore
public abstract AlarmConditionType getType(); public abstract AlarmConditionType getType();

4
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.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
public class NoDataFilterPredicate implements KeyFilterPredicate { public class NoDataFilterPredicate implements KeyFilterPredicate {
@NotNull @NotNull

4
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.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> { public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> {
@NotNull @NotNull

Loading…
Cancel
Save