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();
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) {

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.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<AlarmRuleState, AlarmEvalResult> 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;
}

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 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;
}
}

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.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);
}

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.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<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
public void testChangeAlarmType() throws Exception {
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.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();

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.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

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.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<Double> {
@NotNull

Loading…
Cancel
Save