From 1a66f3973ea5b725a817b3a65ab8db0f4407c874 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Tue, 23 Sep 2025 14:10:03 +0300 Subject: [PATCH] Add repeating alarm condition support for Alarm rules CF --- ...CalculatedFieldEntityMessageProcessor.java | 24 +-- .../cf/AlarmCalculatedFieldResult.java | 17 +-- .../ctx/state/BaseCalculatedFieldState.java | 15 +- .../cf/ctx/state/CalculatedFieldCtx.java | 9 ++ .../cf/ctx/state/CalculatedFieldState.java | 4 +- .../ctx/state/ScriptCalculatedFieldState.java | 4 +- .../ctx/state/SimpleCalculatedFieldState.java | 4 +- .../alarm/AlarmCalculatedFieldState.java | 64 ++++++-- .../cf/ctx/state/alarm/AlarmRuleState.java | 18 +++ .../GeofencingCalculatedFieldState.java | 19 ++- .../thingsboard/server/cf/AlarmRulesTest.java | 140 +++++++++++++++--- .../GeofencingCalculatedFieldStateTest.java | 25 ++-- .../state/ScriptCalculatedFieldStateTest.java | 7 +- .../state/SimpleCalculatedFieldStateTest.java | 11 +- .../utils/CalculatedFieldUtilsTest.java | 2 +- .../condition/DurationAlarmCondition.java | 2 + .../condition/RepeatingAlarmCondition.java | 2 + .../rule/engine/action/TbAlarmResult.java | 20 ++- 18 files changed, 293 insertions(+), 94 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 77665a1f10..1b03c9f7c5 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -122,7 +122,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityInitCalculatedFieldMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing entity init CF msg.", msg.getCtx().getCfId()); + log.debug("[{}] Processing entity init CF msg: {}", msg.getCtx().getCfId(), msg); var ctx = msg.getCtx(); CalculatedFieldState state; if (msg.getStateAction() == StateAction.RECREATE) { @@ -142,11 +142,12 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM state.init(ctx); } if (state.isSizeOk()) { - processStateIfReady(ctx, Collections.singletonList(ctx.getCfId()), state, null, null, msg.getCallback()); + processStateIfReady(ctx, Collections.emptyMap(), Collections.singletonList(ctx.getCfId()), state, null, null, msg.getCallback()); } else { throw new RuntimeException(ctx.getSizeExceedsLimitMessage()); } } catch (Exception e) { + log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -176,7 +177,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); @@ -191,7 +192,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } public void process(EntityCalculatedFieldLinkedTelemetryMsg msg) throws CalculatedFieldException { - log.debug("[{}] Processing CF link telemetry msg.", msg.getEntityId()); + log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg); var proto = msg.getProto(); var ctx = msg.getCtx(); var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); @@ -213,6 +214,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process linked CF telemetry msg: {}", entityId, ctx.getCfId(), msg, e); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } } @@ -235,6 +237,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } } catch (Exception e) { + log.debug("[{}][{}] Failed to process CF telemetry msg: {}", entityId, ctx.getCfId(), proto, e); if (e instanceof CalculatedFieldException cfe) { throw cfe; } @@ -305,10 +308,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } if (state.isSizeOk()) { - if (state.update(ctx, newArgValues) || justRestored) { + Map updatedArgs = state.update(newArgValues, ctx); + if (!updatedArgs.isEmpty() || justRestored) { cfIdList = new ArrayList<>(cfIdList); cfIdList.add(ctx.getCfId()); - processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback); + processStateIfReady(ctx, updatedArgs, cfIdList, state, tbMsgId, tbMsgType, callback); } else { callback.onSuccess(CALLBACKS_PER_CF); } @@ -327,7 +331,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM state.init(ctx); Map arguments = fetchArguments(ctx); - state.update(ctx, arguments); + state.update(arguments, ctx); state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); states.put(ctx.getCfId(), state); @@ -343,12 +347,14 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM return argumentsFuture.get(1, TimeUnit.MINUTES); } - private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + private void processStateIfReady(CalculatedFieldCtx ctx, Map updatedArgs, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { + log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + log.trace("[{}][{}] Performing calculation. Updated args: {}", entityId, ctx.getCfId(), updatedArgs); + CalculatedFieldResult calculationResult = state.performCalculation(updatedArgs, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java index de48d05630..61f9cf37ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.cf; import lombok.Builder; import lombok.Data; +import lombok.RequiredArgsConstructor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbAlarmResult; import org.thingsboard.server.common.data.DataConstants; @@ -25,16 +26,15 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmRuleState; import java.util.List; @Data @Builder +@RequiredArgsConstructor public class AlarmCalculatedFieldResult implements CalculatedFieldResult { private final TbAlarmResult alarmResult; - private final AlarmRuleState alarmRuleState; @Override public TbMsg toTbMsg(EntityId entityId, List cfIds) { @@ -49,14 +49,11 @@ public class AlarmCalculatedFieldResult implements CalculatedFieldResult { } else { metaData.putValue(DataConstants.IS_CLEARED_ALARM, Boolean.TRUE.toString()); } - switch (alarmRuleState.getCondition().getType()) { - case REPEATING -> { - metaData.putValue(DataConstants.ALARM_CONDITION_REPEATS, String.valueOf(alarmRuleState.getEventCount())); - } - case DURATION -> { - // TODO: schedule instead of duration - metaData.putValue(DataConstants.ALARM_CONDITION_DURATION, String.valueOf(alarmRuleState.getDuration())); - } + if (alarmResult.getConditionRepeats() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_REPEATS, String.valueOf(alarmResult.getConditionRepeats())); + } + if (alarmResult.getConditionDuration() != null) { + metaData.putValue(DataConstants.ALARM_CONDITION_DURATION, String.valueOf(alarmResult.getConditionDuration())); } return TbMsg.newMsg() diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index bd6d5b1a51..3baebc3dab 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.utils.CalculatedFieldUtils; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,8 +45,8 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } @Override - public boolean update(CalculatedFieldCtx ctx, Map argumentValues) { - boolean stateUpdated = false; + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + Map updatedArguments = null; for (Map.Entry entry : argumentValues.entrySet()) { String key = entry.getKey(); @@ -65,13 +66,19 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } if (entryUpdated) { - stateUpdated = true; + if (updatedArguments == null) { + updatedArguments = new HashMap<>(argumentValues.size()); + } + updatedArguments.put(key, newEntry); updateLastUpdateTimestamp(newEntry); } } - return stateUpdated; + if (updatedArguments == null) { + updatedArguments = Collections.emptyMap(); + } + return updatedArguments; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 564e573765..1d008c77b8 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 @@ -455,4 +455,13 @@ public class CalculatedFieldCtx { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } + @Override + public String toString() { + return "CalculatedFieldCtx{" + + "cfId=" + cfId + + ", cfType=" + cfType + + ", entityId=" + entityId + + '}'; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index b397a8e87d..d7c061faba 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -56,11 +56,11 @@ public interface CalculatedFieldState { void init(CalculatedFieldCtx ctx); - boolean update(CalculatedFieldCtx ctx, Map arguments); + Map update(Map arguments, CalculatedFieldCtx ctx); void reset(CalculatedFieldCtx ctx); - ListenableFuture performCalculation(CalculatedFieldCtx ctx); + ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx); @JsonIgnore boolean isReady(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 13eaa69ca7..3b6f9b1f87 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -27,6 +27,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Map; + @Slf4j @EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { @@ -41,7 +43,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { ListenableFuture resultFuture = ctx.evaluateTbelExpression(ctx.getExpression(), this); Output output = ctx.getOutput(); return Futures.transform(resultFuture, diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 13aa3fe6fd..2dc8e1824a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Map; + @EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { @@ -48,7 +50,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { double expressionResult = ctx.evaluateSimpleExpression(ctx.getExpression(), this); Output output = ctx.getOutput(); 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 747846f655..f7da5f32fa 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 @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; import org.thingsboard.server.common.data.alarm.rule.condition.expression.AlarmConditionExpression; import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression; import org.thingsboard.server.common.data.audit.ActionType; @@ -104,23 +105,36 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { log.debug("Initialized create rule states {} and clear rule state {} for {}", createRuleStates, clearRuleState, ctx.getCalculatedField()); } + @Override + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + return super.update(argumentValues, ctx); + } + @Override public void reset(CalculatedFieldCtx ctx) { super.reset(ctx); } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { + if (updatedArgs.isEmpty()) { + // FIXME: do we evaluate alarm rule (and increment event count) after arguments or expression change (state reinit)??? + return Futures.immediateFuture(new AlarmCalculatedFieldResult(null)); + } initCurrentAlarm(ctx); - AlarmCalculatedFieldResult result = createOrClearAlarms(state -> state.eval(ctx), ctx); - return Futures.immediateFuture(result); + TbAlarmResult result = createOrClearAlarms(state -> state.eval(ctx), ctx); + return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() + .alarmResult(result) + .build()); } // TODO: harvesting - public ListenableFuture performCalculation(long ts, CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(Map updatedArgs, long ts, CalculatedFieldCtx ctx) { initCurrentAlarm(ctx); - AlarmCalculatedFieldResult result = createOrClearAlarms(ruleState -> ruleState.eval(ts), ctx); - return Futures.immediateFuture(result); + TbAlarmResult result = createOrClearAlarms(ruleState -> ruleState.eval(ts), ctx); + return Futures.immediateFuture(AlarmCalculatedFieldResult.builder() + .alarmResult(result) + .build()); } @SneakyThrows @@ -160,28 +174,33 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { createRuleStates.values().forEach(AlarmRuleState::clear); } - public AlarmCalculatedFieldResult createOrClearAlarms(Function evalFunction, CalculatedFieldCtx ctx) { + private TbAlarmResult createOrClearAlarms(Function evalFunction, + CalculatedFieldCtx ctx) { TbAlarmResult result = null; AlarmRuleState resultState = null; + AlarmRuleState.StateInfo resultStateInfo = null; + for (AlarmRuleState state : createRuleStates.values()) { AlarmEvalResult evalResult = evalFunction.apply(state); log.debug("Evaluated create rule {} with args {}. Result: {}", state, arguments, evalResult); - if (AlarmEvalResult.TRUE.equals(evalResult)) { + if (evalResult == AlarmEvalResult.TRUE) { resultState = state; break; - } else if (AlarmEvalResult.FALSE.equals(evalResult)) { + } else if (evalResult == AlarmEvalResult.FALSE) { clearAlarmState(state); } } if (resultState != null) { result = calculateAlarmResult(resultState, ctx); + resultStateInfo = resultState.getStateInfo(); log.debug("Alarm result for state {}: {}", resultState, result); clearAlarmState(clearRuleState); } else if (currentAlarm != null && clearRuleState != null) { AlarmEvalResult evalResult = evalFunction.apply(clearRuleState); log.debug("Evaluated clear rule {} with args {}. Result: {}", clearRuleState, arguments, evalResult); - if (AlarmEvalResult.TRUE.equals(evalResult)) { + if (evalResult == AlarmEvalResult.TRUE) { + resultStateInfo = clearRuleState.getStateInfo(); clearAlarmState(clearRuleState); for (AlarmRuleState state : createRuleStates.values()) { clearAlarmState(state); @@ -190,18 +209,23 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), true ); if (clearResult.isCleared()) { - result = new TbAlarmResult(false, false, true, clearResult.getAlarm()); + result = TbAlarmResult.builder() + .isCleared(true) + .alarm(clearResult.getAlarm()) + .build(); + addStateInfo(result, clearRuleState); resultState = clearRuleState; } currentAlarm = null; - } else if (AlarmEvalResult.FALSE.equals(evalResult)) { + } else if (evalResult == AlarmEvalResult.FALSE) { clearAlarmState(clearRuleState); } } - return AlarmCalculatedFieldResult.builder() - .alarmResult(result) - .alarmRuleState(resultState) - .build(); + if (result != null && resultState != null) { + result.setConditionRepeats(resultStateInfo.eventCount()); + result.setConditionDuration(resultStateInfo.duration()); + } + return result; } private void clearAlarmState(AlarmRuleState state) { @@ -265,6 +289,14 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { } } + private void addStateInfo(TbAlarmResult alarmResult, AlarmRuleState ruleState) { + if (ruleState.getCondition().getType() == AlarmConditionType.REPEATING) { + alarmResult.setConditionRepeats(ruleState.getEventCount()); + } else if (ruleState.getCondition().getType() == AlarmConditionType.DURATION) { + alarmResult.setConditionDuration(ruleState.getDuration()); + } + } + private JsonNode createDetails(AlarmRuleState ruleState) { JsonNode alarmDetails; String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails(); 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 04386c68f6..2e971ffebb 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 @@ -22,6 +22,7 @@ import org.thingsboard.server.common.adaptor.JsonConverter; 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.AlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionType; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; @@ -194,6 +195,8 @@ public class AlarmRuleState { } private long getRequiredDurationInMs() { + // fixme timeUnit?? + return getValue(((DurationAlarmCondition) condition).getValue(), KvUtil::getLongValue); } @@ -219,6 +222,16 @@ public class AlarmRuleState { this.condition = alarmRule.getCondition(); } + public StateInfo getStateInfo() { + if (condition.getType() == AlarmConditionType.REPEATING) { + return new StateInfo(eventCount, null); + } else if (condition.getType() == AlarmConditionType.DURATION) { + return new StateInfo(null, duration); + } else { + return StateInfo.EMPTY; + } + } + @Override public String toString() { return "AlarmRuleState{" + @@ -230,4 +243,9 @@ public class AlarmRuleState { '}'; } + public record StateInfo(Long eventCount, Long duration) { + static final StateInfo EMPTY = new StateInfo(null, null); + + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index b418dc73d8..ad31d23702 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -41,6 +41,8 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -68,8 +70,8 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public boolean update(CalculatedFieldCtx ctx, Map argumentValues) { - boolean stateUpdated = false; + public Map update(Map argumentValues, CalculatedFieldCtx ctx) { + Map updatedArguments = null; for (var entry : argumentValues.entrySet()) { String key = entry.getKey(); @@ -103,14 +105,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { entryUpdated = existingEntry.updateEntry(newEntry); } if (entryUpdated) { - stateUpdated = true; + if (updatedArguments == null) { + updatedArguments = new HashMap<>(argumentValues.size()); + } + updatedArguments.put(key, newEntry); } } - return stateUpdated; + + if (updatedArguments == null) { + updatedArguments = Collections.emptyMap(); + } + return updatedArguments; } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(Map updatedArgs, CalculatedFieldCtx ctx) { double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); Coordinates entityCoordinates = new Coordinates(latitude, longitude); 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 5cf674f6b6..166589038c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.cf; +import lombok.extern.slf4j.Slf4j; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; @@ -23,11 +24,15 @@ import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbAlarmResult; import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.rule.AlarmRule; +import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; +import org.thingsboard.server.common.data.alarm.rule.condition.DurationAlarmCondition; +import org.thingsboard.server.common.data.alarm.rule.condition.RepeatingAlarmCondition; 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.cf.CalculatedField; @@ -56,6 +61,7 @@ import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static org.testcontainers.shaded.org.awaitility.Awaitility.await; +@Slf4j @DaoSqlTest public class AlarmRulesTest extends AbstractControllerTest { @@ -79,15 +85,17 @@ public class AlarmRulesTest extends AbstractControllerTest { public void testCreateAndSeverityUpdateAndClear() throws Exception { Argument temperatureArgument = new Argument(); temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); Map arguments = Map.of( "temperature", temperatureArgument ); - Map createRules = Map.of( - AlarmSeverity.MAJOR, "return temperature >= 50;", - AlarmSeverity.CRITICAL, "return temperature >= 100;" + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", null, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 100;", null, null) ); - String clearRule = "return temperature <= 25;"; + + Condition clearRule = new Condition("return temperature <= 25;", null, null); CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", arguments, createRules, clearRule); @@ -120,6 +128,71 @@ public class AlarmRulesTest extends AbstractControllerTest { }); } + /* + * todo: state restore (event count) + * */ + @Test + public void testCreateAlarmForRepeatingConditionOnTs() throws Exception { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + temperatureArgument.setDefaultValue("0"); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + int eventsCountMajor = 5; + int eventsCountCritical = 10; + Map createRules = Map.of( + AlarmSeverity.MAJOR, new Condition("return temperature >= 50;", eventsCountMajor, null), + AlarmSeverity.CRITICAL, new Condition("return temperature >= 50;", eventsCountCritical, null) + ); + + CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", + arguments, createRules, null); + for (int i = 0; i < 4; i++) { + postTelemetry(deviceId, "{\"temperature\":50}"); + Thread.sleep(10); + } + assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); + 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); + assertThat(alarmResult.getConditionRepeats()).isEqualTo(5); + }); + } + + @Test + public void testCreateAlarmForRepeatingConditionOnAttribute() { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.ATTRIBUTE, AttributeScope.SHARED_SCOPE)); + Map arguments = Map.of( + "temperature", temperatureArgument + ); + + Map createRules = Map.of( + AlarmSeverity.MAJOR, "return temperature >= 50;", + AlarmSeverity.CRITICAL, "return temperature >= 100;" + ); + String clearRule = "return temperature <= 25;"; +// CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature Alarm", +// arguments, createRules, clearRule); + } + + @Test + public void testCreateAlarmForDurationCondition() { + Argument temperatureArgument = new Argument(); + temperatureArgument.setRefEntityKey(new ReferencedEntityKey("powerConsumption", ArgumentType.TS_LATEST, null)); + Map arguments = Map.of( + "powerConsumption", temperatureArgument + ); + + +// CalculatedField calculatedField = createAlarmCf(deviceId, "High power consumption during 5 seconds", +// arguments, createRules, nu); + } + private void checkAlarmResult(CalculatedField calculatedField, Consumer assertion) { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { TbAlarmResult alarmResult = getLatestAlarmResult(calculatedField.getId()); @@ -152,34 +225,61 @@ public class AlarmRulesTest extends AbstractControllerTest { private CalculatedField createAlarmCf(EntityId entityId, String alarmType, Map arguments, - Map createConditions, - String clearCondition) { + Map createConditions, + Condition clearCondition) { + Map createRules = new HashMap<>(); + createConditions.forEach((severity, condition) -> { + createRules.put(severity, toAlarmRule(condition)); + }); + AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; + CalculatedField calculatedField = createAlarmCf(entityId, alarmType, arguments, createRules, clearRule); + + CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> getDebugEvents(calculatedField.getId(), 1), events -> !events.isEmpty()).get(0); + latestEventId = debugEvent.getId(); + return calculatedField; + } + + private CalculatedField createAlarmCf(EntityId entityId, + String alarmType, + Map arguments, + Map createRules, + AlarmRule clearRule) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(entityId); calculatedField.setName(alarmType); calculatedField.setType(CalculatedFieldType.ALARM); AlarmCalculatedFieldConfiguration configuration = new AlarmCalculatedFieldConfiguration(); configuration.setArguments(arguments); - configuration.setCreateRules(new HashMap<>()); - createConditions.forEach((severity, expression) -> { - configuration.getCreateRules().put(severity, toAlarmRule(expression)); - }); - configuration.setClearRule(toAlarmRule(clearCondition)); + configuration.setCreateRules(createRules); + configuration.setClearRule(clearRule); calculatedField.setConfiguration(configuration); calculatedField.setDebugSettings(DebugSettings.all()); return saveCalculatedField(calculatedField); } - private AlarmRule toAlarmRule(String conditionExpression) { - if (conditionExpression == null) { - return null; - } + private AlarmRule toAlarmRule(Condition condition) { AlarmRule rule = new AlarmRule(); - SimpleAlarmCondition condition = new SimpleAlarmCondition(); TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); - expression.setExpression(conditionExpression); - condition.setExpression(expression); - rule.setCondition(condition); + expression.setExpression(condition.expression()); + if (condition.eventsCount() != null) { + RepeatingAlarmCondition alarmCondition = new RepeatingAlarmCondition(); + alarmCondition.setExpression(expression); + AlarmConditionValue count = new AlarmConditionValue<>(); + count.setStaticValue(condition.eventsCount()); + alarmCondition.setCount(count); + rule.setCondition(alarmCondition); + } else if (condition.durationMs() != null) { + DurationAlarmCondition alarmCondition = new DurationAlarmCondition(); + alarmCondition.setExpression(expression); + AlarmConditionValue duration = new AlarmConditionValue<>(); + duration.setStaticValue(condition.durationMs()); + alarmCondition.setValue(duration); + rule.setCondition(alarmCondition); + } else { + SimpleAlarmCondition alarmCondition = new SimpleAlarmCondition(); + alarmCondition.setExpression(expression); + rule.setCondition(alarmCondition); + } return rule; } @@ -188,4 +288,6 @@ public class AlarmRulesTest extends AbstractControllerTest { .map(e -> (CalculatedFieldDebugEvent) e).toList(); } + private record Condition(String expression, Integer eventsCount, Long durationMs) {} + } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index d3bb7206f3..b88442dc62 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -49,6 +49,7 @@ import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,7 +119,7 @@ public class GeofencingCalculatedFieldStateTest { )); Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); - boolean stateUpdated = state.update(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -132,21 +133,21 @@ public class GeofencingCalculatedFieldStateTest { @Test void testUpdateStateWithInvalidArgumentTypeForLatitudeArgument() { - assertThatThrownBy(() -> state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); } @Test void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() { - assertThatThrownBy(() -> state.update(ctx, Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); } @Test void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() { - assertThatThrownBy(() -> state.update(ctx, Map.of("someArgumentName", latitudeArgEntry))) + assertThatThrownBy(() -> state.update(Map.of("someArgumentName", latitudeArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for someArgumentName argument: SINGLE_VALUE. Only GEOFENCING type is allowed."); } @@ -157,7 +158,7 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 190L); Map newArgs = Map.of("latitude", newArgEntry); - boolean stateUpdated = state.update(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).isEqualTo(newArgs); @@ -169,7 +170,7 @@ public class GeofencingCalculatedFieldStateTest { Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); - boolean stateUpdated = state.update(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isFalse(); assertThat(state.getArguments()).isEqualTo(newArgs); @@ -179,7 +180,7 @@ public class GeofencingCalculatedFieldStateTest { void testUpdateStateWhenUpdateExistingSingleValueArgumentEntryWithValueOfAnotherType() { state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); - assertThatThrownBy(() -> state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + assertThatThrownBy(() -> state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for single value argument entry: GEOFENCING"); } @@ -189,7 +190,7 @@ public class GeofencingCalculatedFieldStateTest { void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithValueOfAnotherType() { state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); - assertThatThrownBy(() -> state.update(ctx, Map.of("allowedZones", latitudeArgEntry))) + assertThatThrownBy(() -> state.update(Map.of("allowedZones", latitudeArgEntry), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); } @@ -255,7 +256,7 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); // move the device to new coordinates → leaves allowed, enters restricted - state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); TelemetryCalculatedFieldResult result2 = performCalculation(); @@ -327,7 +328,7 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); // move the device to new coordinates → leaves allowed, enters restricted - state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); TelemetryCalculatedFieldResult result2 = performCalculation(); @@ -399,7 +400,7 @@ public class GeofencingCalculatedFieldStateTest { SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); // move the device to new coordinates → leaves allowed, enters restricted - state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + state.update(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude), ctx); TelemetryCalculatedFieldResult result2 = performCalculation(); @@ -486,7 +487,7 @@ public class GeofencingCalculatedFieldStateTest { } private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { - return (TelemetryCalculatedFieldResult) state.performCalculation(ctx).get(); + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); } } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 972d83f8a8..56fc2c1086 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -45,6 +45,7 @@ import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -99,7 +100,7 @@ public class ScriptCalculatedFieldStateTest { state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry)); Map newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry); - boolean stateUpdated = state.update(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -116,7 +117,7 @@ public class ScriptCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L); Map newArgs = Map.of("assetHumidity", newArgEntry); - boolean stateUpdated = state.update(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -228,7 +229,7 @@ public class ScriptCalculatedFieldStateTest { } private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { - return (TelemetryCalculatedFieldResult) state.performCalculation(ctx).get(); + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); } } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 376b57d9d2..00e3ed71f8 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -94,7 +95,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", key3ArgEntry); - boolean stateUpdated = state.update(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( @@ -112,7 +113,7 @@ public class SimpleCalculatedFieldStateTest { SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L); Map newArgs = Map.of("key1", newArgEntry); - boolean stateUpdated = state.update(ctx, newArgs); + boolean stateUpdated = !state.update(newArgs, ctx).isEmpty(); assertThat(stateUpdated).isTrue(); assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry)); @@ -126,7 +127,7 @@ public class SimpleCalculatedFieldStateTest { )); Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); - assertThatThrownBy(() -> state.update(ctx, newArgs)) + assertThatThrownBy(() -> state.update(newArgs, ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Rolling argument entry is not supported for simple calculated fields."); } @@ -156,7 +157,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - assertThatThrownBy(() -> state.performCalculation(ctx)) + assertThatThrownBy(() -> state.performCalculation(Collections.emptyMap(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Argument 'key2' is not a number."); } @@ -271,7 +272,7 @@ public class SimpleCalculatedFieldStateTest { } private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException { - return (TelemetryCalculatedFieldResult) state.performCalculation(ctx).get(); + return (TelemetryCalculatedFieldResult) state.performCalculation(Collections.emptyMap(), ctx).get(); } } \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index acdf7bbf36..40a7a14e1c 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -85,7 +85,7 @@ class CalculatedFieldUtilsTest { // Create cf state with the geofencing argument and add it to the state map CalculatedFieldState state = new GeofencingCalculatedFieldState(DEVICE_ID); - state.update(mock(CalculatedFieldCtx.class), Map.of("geofencingArgumentTest", geofencingArgumentEntry)); + state.update(Map.of("geofencingArgumentTest", geofencingArgumentEntry), mock(CalculatedFieldCtx.class)); // when CalculatedFieldStateProto proto = toProto(stateId, state); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java index 7656d63bc0..6210bd6b59 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/DurationAlarmCondition.java @@ -17,11 +17,13 @@ package org.thingsboard.server.common.data.alarm.rule.condition; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.ToString; import java.util.concurrent.TimeUnit; @Data @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) public class DurationAlarmCondition extends AlarmCondition { private TimeUnit unit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java index 9a57bb4631..cdf474c4dc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/RepeatingAlarmCondition.java @@ -17,9 +17,11 @@ package org.thingsboard.server.common.data.alarm.rule.condition; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.ToString; @Data @EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) public class RepeatingAlarmCondition extends AlarmCondition { private AlarmConditionValue count; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java index f594d69eab..d25846c984 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.action; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.Alarm; @@ -24,13 +25,18 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult; @Data @AllArgsConstructor @NoArgsConstructor +@Builder public class TbAlarmResult { + boolean isCreated; boolean isUpdated; boolean isSeverityUpdated; boolean isCleared; Alarm alarm; + Long conditionRepeats; + Long conditionDuration; + public TbAlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) { this.isCreated = isCreated; this.isUpdated = isUpdated; @@ -40,11 +46,13 @@ public class TbAlarmResult { public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) { boolean isSeverityChanged = result.isSeverityChanged(); - return new TbAlarmResult( - result.isCreated(), - result.isModified() && !isSeverityChanged, - isSeverityChanged, - result.isCleared(), - result.getAlarm()); + return TbAlarmResult.builder() + .isCreated(result.isCreated()) + .isUpdated(result.isModified() && !isSeverityChanged) + .isSeverityUpdated(isSeverityChanged) + .isCleared(result.isCleared()) + .alarm(result.getAlarm()) + .build(); } + }