Browse Source

Merge branch 'feature/entity-alarm-rules' of github.com:thingsboard/thingsboard into feature/aggregation-cf

pull/14141/head
IrynaMatveieva 9 months ago
parent
commit
11e4e2f17b
  1. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  2. 32
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  3. 34
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  4. 56
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  5. 14
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  6. 22
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  7. 1
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  8. 18
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  9. 63
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  10. 35
      application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java
  11. 2
      application/src/test/resources/logback-test.xml
  12. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  13. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java
  14. 1
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java
  15. 8
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java
  16. 2
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java
  17. 5
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java
  18. 5
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java
  19. 6
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java
  20. 35
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java
  21. 3
      common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java
  22. 2
      common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java
  23. 29
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  24. 2
      common/proto/src/main/proto/queue.proto
  25. 2
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  26. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  27. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -860,8 +860,9 @@ public class ActorSystemContext {
if (errorMessage != null) { if (errorMessage != null) {
eventBuilder.error(errorMessage); eventBuilder.error(errorMessage);
} }
CalculatedFieldDebugEvent event = eventBuilder.build();
ListenableFuture<Void> future = eventService.saveAsync(eventBuilder.build()); log.debug("Persisting calculated field debug event: {}", event);
ListenableFuture<Void> future = eventService.saveAsync(event);
Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor()); Futures.addCallback(future, CALCULATED_FIELD_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
log.warn("Failed to persist calculated field debug message", ex); log.warn("Failed to persist calculated field debug message", ex);

32
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -175,7 +175,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
public void process(CalculatedFieldArgumentResetMsg msg) throws CalculatedFieldException { public void process(CalculatedFieldArgumentResetMsg msg) throws CalculatedFieldException {
log.debug("[{}] Processing CF argument reset msg.", entityId); log.debug("[{}] Processing CF argument reset msg.", entityId);
var ctx = msg.getCtx(); var ctx = msg.getCtx();
var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback());
try { try {
Map<String, Argument> dynamicSourceArgs = ctx.getArguments().entrySet().stream() Map<String, Argument> dynamicSourceArgs = ctx.getArguments().entrySet().stream()
.filter(entry -> entry.getValue().hasOwnerSource()) .filter(entry -> entry.getValue().hasOwnerSource())
@ -184,7 +183,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
Map<String, ArgumentEntry> fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs); Map<String, ArgumentEntry> fetchedArgs = cfService.fetchArgsFromDb(tenantId, entityId, dynamicSourceArgs);
fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true)); fetchedArgs.values().forEach(arg -> arg.setForceResetPrevious(true));
processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), callback, fetchedArgs, null, null); processArgumentValuesUpdate(ctx, Collections.singletonList(ctx.getCfId()), msg.getCallback(), fetchedArgs, null, null);
} catch (Exception e) { } catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
} }
@ -277,7 +276,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException { public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException {
log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg); log.trace("[{}] Processing CF telemetry msg: {}", msg.getEntityId(), msg);
var proto = msg.getProto(); var proto = msg.getProto();
var numberOfCallbacks = CALLBACKS_PER_CF * (msg.getEntityIdFields().size() + msg.getProfileIdFields().size()); var numberOfCallbacks = msg.getEntityIdFields().size() + msg.getProfileIdFields().size();
MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback()); MultipleTbCallback callback = new MultipleTbCallback(numberOfCallbacks, msg.getCallback());
List<CalculatedFieldId> cfIdList = getCalculatedFieldIds(proto); List<CalculatedFieldId> cfIdList = getCalculatedFieldIds(proto);
Set<CalculatedFieldId> cfIdSet = new HashSet<>(cfIdList); Set<CalculatedFieldId> cfIdSet = new HashSet<>(cfIdList);
@ -293,11 +292,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg); log.trace("[{}] Processing CF link telemetry msg: {}", msg.getEntityId(), msg);
var proto = msg.getProto(); var proto = msg.getProto();
var ctx = msg.getCtx(); var ctx = msg.getCtx();
var callback = new MultipleTbCallback(CALLBACKS_PER_CF, msg.getCallback()); var callback = msg.getCallback();
try { try {
List<CalculatedFieldId> cfIds = getCalculatedFieldIds(proto); List<CalculatedFieldId> cfIds = getCalculatedFieldIds(proto);
if (cfIds.contains(ctx.getCfId())) { if (cfIds.contains(ctx.getCfId())) {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} else { } else {
if (proto.getTsDataCount() > 0) { if (proto.getTsDataCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIds, callback, mapToArguments(ctx, msg.getEntityId(), proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
@ -308,7 +307,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else if (proto.getRemovedAttrKeysCount() > 0) { } else if (proto.getRemovedAttrKeysCount() > 0) {
processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIds, callback, mapToArgumentsWithDefaultValue(ctx, msg.getEntityId(), proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} else { } else {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -317,10 +316,10 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} }
} }
private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection<CalculatedFieldId> cfIds, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void process(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, Collection<CalculatedFieldId> cfIds, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
try { try {
if (cfIds.contains(ctx.getCfId())) { if (cfIds.contains(ctx.getCfId())) {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} else { } else {
if (proto.getTsDataCount() > 0) { if (proto.getTsDataCount() > 0) {
processTelemetry(ctx, proto, cfIdList, callback); processTelemetry(ctx, proto, cfIdList, callback);
@ -331,7 +330,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} else if (proto.getRemovedAttrKeysCount() > 0) { } else if (proto.getRemovedAttrKeysCount() > 0) {
processRemovedAttributes(ctx, proto, cfIdList, callback); processRemovedAttributes(ctx, proto, cfIdList, callback);
} else { } else {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
} }
} catch (Exception e) { } catch (Exception e) {
@ -371,27 +370,27 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
msg.getCallback().onSuccess(); msg.getCallback().onSuccess();
} }
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getScope(), proto.getAttrDataList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processRemovedTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, entityId, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithFetchedValue(ctx, entityId, proto.getRemovedTsKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { private void processRemovedAttributes(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, TbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto)); processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArgumentsWithDefaultValue(ctx, proto.getScope(), proto.getRemovedAttrKeysList()), toTbMsgId(proto), toTbMsgType(proto));
} }
private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback, private void processArgumentValuesUpdate(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, TbCallback callback,
Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException { Map<String, ArgumentEntry> newArgValues, UUID tbMsgId, TbMsgType tbMsgType) throws CalculatedFieldException {
if (newArgValues.isEmpty()) { if (newArgValues.isEmpty()) {
log.debug("[{}] No new argument values to process for CF.", ctx.getCfId()); log.debug("[{}] No new argument values to process for CF.", ctx.getCfId());
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
CalculatedFieldState state = states.get(ctx.getCfId()); CalculatedFieldState state = states.get(ctx.getCfId());
boolean justRestored = false; boolean justRestored = false;
@ -418,7 +417,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
cfIdList.add(ctx.getCfId()); cfIdList.add(ctx.getCfId());
processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback); processStateIfReady(state, updatedArgs, ctx, cfIdList, tbMsgId, tbMsgType, callback);
} else { } else {
callback.onSuccess(CALLBACKS_PER_CF); callback.onSuccess();
} }
} else { } else {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(); throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
@ -459,6 +458,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx, private void processStateIfReady(CalculatedFieldState state, Map<String, ArgumentEntry> updatedArgs, CalculatedFieldCtx ctx,
List<CalculatedFieldId> cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { List<CalculatedFieldId> cfIdList, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException {
callback = new MultipleTbCallback(CALLBACKS_PER_CF, callback);
log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs); log.trace("[{}][{}] Processing state if ready. Current args: {}, updated args: {}", entityId, ctx.getCfId(), state.getArguments(), updatedArgs);
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId); CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
boolean stateSizeChecked = false; boolean stateSizeChecked = false;

34
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java

@ -134,7 +134,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void stop() { public void stop() {
log.info("[{}] Stopping CF manager actor.", tenantId); log.info("[{}] Stopping CF manager actor.", tenantId);
calculatedFields.values().forEach(CalculatedFieldCtx::stop); calculatedFields.values().forEach(CalculatedFieldCtx::close);
calculatedFields.clear(); calculatedFields.clear();
entityIdCalculatedFields.clear(); entityIdCalculatedFields.clear();
entityIdCalculatedFieldLinks.clear(); entityIdCalculatedFieldLinks.clear();
@ -398,7 +398,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId);
callback.onSuccess(); callback.onSuccess();
} else { } else {
var newCfCtx = getCfCtx(newCf); // fixme wtf? why isn't oldCfCtx closed properly? when to close it? var newCfCtx = getCfCtx(newCf);
try { try {
newCfCtx.init(); newCfCtx.init();
} catch (Exception e) { } catch (Exception e) {
@ -441,14 +441,26 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return; return;
} }
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb)); applyToTargetCfEntityActors(newCfCtx, new TbCallback() {
@Override
public void onSuccess() {
oldCfCtx.close();
callback.onSuccess();
}
@Override
public void onFailure(Throwable t) {
oldCfCtx.close();
callback.onFailure(t);
}
}, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb));
} }
} }
} }
private void onCfDeleted(ComponentLifecycleMsg msg, TbCallback callback) { private void onCfDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var cfId = new CalculatedFieldId(msg.getEntityId().getId());
var cfCtx = calculatedFields.remove(cfId); // fixme wtf? why isn't ctx closed properly? var cfCtx = calculatedFields.remove(cfId);
aggCalculatedFields.remove(cfId); aggCalculatedFields.remove(cfId);
if (cfCtx == null) { if (cfCtx == null) {
log.debug("[{}] CF was already deleted [{}]", tenantId, cfId); log.debug("[{}] CF was already deleted [{}]", tenantId, cfId);
@ -457,7 +469,19 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
} }
entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx);
deleteLinks(cfCtx); deleteLinks(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); applyToTargetCfEntityActors(cfCtx, new TbCallback() {
@Override
public void onSuccess() {
cfCtx.close();
callback.onSuccess();
}
@Override
public void onFailure(Throwable t) {
cfCtx.close();
callback.onFailure(t);
}
}, (id, cb) -> deleteCfForEntity(id, cfId, cb));
} }
public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) {

56
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -60,6 +60,7 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.io.Closeable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -68,11 +69,10 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@Data @Data
@Slf4j @Slf4j
public class CalculatedFieldCtx { public class CalculatedFieldCtx implements Closeable {
private CalculatedField calculatedField; private CalculatedField calculatedField;
@ -211,15 +211,12 @@ public class CalculatedFieldCtx {
} }
case ALARM -> { case ALARM -> {
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
Stream<AlarmRule> rules = configuration.getCreateRules().values().stream(); configuration.getAllRules().map(rule -> rule.getValue().getCondition().getExpression())
if (configuration.getClearRule() != null) { .forEach(expression -> {
rules = Stream.concat(rules, Stream.of(configuration.getClearRule())); if (expression instanceof TbelAlarmConditionExpression tbelExpression) {
} initTbelExpression(tbelExpression.getExpression());
rules.map(rule -> rule.getCondition().getExpression()).forEach(expression -> { }
if (expression instanceof TbelAlarmConditionExpression tbelExpression) { });
initTbelExpression(tbelExpression.getExpression());
}
});
initialized = true; initialized = true;
} }
case PROPAGATION -> { case PROPAGATION -> {
@ -294,7 +291,6 @@ public class CalculatedFieldCtx {
public ScheduledFuture<?> scheduleReevaluation(long delayMs, TbActorRef actorCtx) { public ScheduledFuture<?> scheduleReevaluation(long delayMs, TbActorRef actorCtx) {
log.debug("[{}] Scheduling CF reevaluation in {} ms", cfId, delayMs); log.debug("[{}] Scheduling CF reevaluation in {} ms", cfId, delayMs);
// TODO: use single lazy-loaded instance of CalculatedFieldReevaluateMsg
return systemContext.scheduleMsgWithDelay(actorCtx, new CalculatedFieldReevaluateMsg(tenantId, this), delayMs); return systemContext.scheduleMsgWithDelay(actorCtx, new CalculatedFieldReevaluateMsg(tenantId, this), delayMs);
} }
@ -578,8 +574,17 @@ public class CalculatedFieldCtx {
if (!Objects.equals(output, other.output)) { if (!Objects.equals(output, other.output)) {
return true; return true;
} }
if (cfType == CalculatedFieldType.ALARM && !calculatedField.getName().equals(other.getCalculatedField().getName())) { if (cfType == CalculatedFieldType.ALARM) {
return true; if (!calculatedField.getName().equals(other.getCalculatedField().getName())) {
return true;
}
var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration();
if (!thisConfig.rulesEqual(otherConfig, AlarmRule::equals)) {
// if the rules have any changes not tracked by hasStateChanges
return true;
}
} }
if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) { if (scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis) {
return true; return true;
@ -599,8 +604,10 @@ public class CalculatedFieldCtx {
if (cfType == CalculatedFieldType.ALARM) { if (cfType == CalculatedFieldType.ALARM) {
var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration(); var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration();
if (!thisConfig.getCreateRules().equals(otherConfig.getCreateRules()) || if (!thisConfig.rulesEqual(otherConfig, (thisRule, otherRule) -> {
!Objects.equals(thisConfig.getClearRule(), otherConfig.getClearRule())) { return thisRule.getCondition().getType() == otherRule.getCondition().getType();
})) {
// reinitializing only if the rule list changed, or if a condition type changed for any rule
return true; return true;
} }
} }
@ -654,12 +661,17 @@ public class CalculatedFieldCtx {
}; };
} }
public void stop() { @Override
if (tbelExpressions != null) { public void close() {
tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy); try {
} if (tbelExpressions != null) {
if (simpleExpressions != null) { tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy);
simpleExpressions.values().forEach(ThreadLocal::remove); }
if (simpleExpressions != null) {
simpleExpressions.values().forEach(ThreadLocal::remove);
}
} catch (Exception e) {
log.warn("Failed to stop {}", this, e);
} }
} }

14
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java

@ -103,6 +103,18 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
this.configuration = getConfiguration(ctx); this.configuration = getConfiguration(ctx);
this.alarmType = ctx.getCalculatedField().getName(); this.alarmType = ctx.getCalculatedField().getName();
Map<AlarmSeverity, AlarmRule> createRules = configuration.getCreateRules();
createRules.forEach((severity, rule) -> {
AlarmRuleState ruleState = createRuleStates.get(severity);
if (ruleState != null) {
ruleState.setAlarmRule(rule);
}
});
AlarmRule clearRule = configuration.getClearRule();
if (clearRule != null && clearRuleState != null) {
clearRuleState.setAlarmRule(clearRule);
}
if (currentAlarm != null && !currentAlarm.getType().equals(alarmType)) { if (currentAlarm != null && !currentAlarm.getType().equals(alarmType)) {
currentAlarm = null; currentAlarm = null;
initialFetchDone = false; initialFetchDone = false;
@ -265,7 +277,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
clearState(state); clearState(state);
} }
AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm( AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm(
ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), true ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), false
); );
if (clearResult.isCleared()) { if (clearResult.isCleared()) {
result = TbAlarmResult.builder() result = TbAlarmResult.builder()

22
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java

@ -237,7 +237,15 @@ public class AlarmRuleState {
} }
public void clear() { public void clear() {
clearRepeatingConditionState();
clearDurationConditionState();
}
private void clearRepeatingConditionState() {
eventCount = 0L; eventCount = 0L;
}
private void clearDurationConditionState() {
firstEventTs = 0L; firstEventTs = 0L;
lastEventTs = 0L; lastEventTs = 0L;
duration = 0L; duration = 0L;
@ -289,6 +297,20 @@ public class AlarmRuleState {
public void setAlarmRule(AlarmRule alarmRule) { public void setAlarmRule(AlarmRule alarmRule) {
this.alarmRule = alarmRule; this.alarmRule = alarmRule;
this.condition = alarmRule.getCondition(); this.condition = alarmRule.getCondition();
// clearing state for other condition types (possibly left from a previous condition type)
switch (condition.getType()) {
case SIMPLE -> {
clearRepeatingConditionState();
clearDurationConditionState();
}
case REPEATING -> {
clearDurationConditionState();
}
case DURATION -> {
clearRepeatingConditionState();
}
}
} }
public StateInfo getStateInfo() { public StateInfo getStateInfo() {

1
application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java

@ -257,7 +257,6 @@ public class EntityStateSourcingListener {
if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) { if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) {
ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder() ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder()
.setEventMsg(toProto(event)) .setEventMsg(toProto(event))
.addCfTypes(CalculatedFieldType.ALARM.name())
.build(); .build();
tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() { tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() {
@Override @Override

18
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.queue; package org.thingsboard.server.service.queue;
import com.google.protobuf.ProtocolStringList;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -28,7 +27,6 @@ import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTeleme
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -61,7 +59,6 @@ import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedCon
import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -183,8 +180,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
} }
private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) { private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) {
Set<CalculatedFieldType> cfTypes = getCfTypes(toCfMsg.getCfTypesList()); if (toCfMsg.hasTelemetryMsg()) {
if (toCfMsg.hasTelemetryMsg()) { // TODO: add CF type filter to the message. or just rename the CF strategy to "Process alarms and calculated fields
log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg()); log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg());
forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback);
} else if (toCfMsg.hasLinkedTelemetryMsg()) { } else if (toCfMsg.hasLinkedTelemetryMsg()) {
@ -264,18 +260,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB)); return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB));
} }
private Set<CalculatedFieldType> getCfTypes(ProtocolStringList cfTypesList) {
Set<CalculatedFieldType> cfTypes;
if (cfTypesList.isEmpty()) {
cfTypes = EnumSet.allOf(CalculatedFieldType.class);
} else {
cfTypes = cfTypesList.stream()
.map(CalculatedFieldType::valueOf)
.collect(Collectors.toSet());
}
return cfTypes;
}
@Override @Override
protected void stopConsumers() { protected void stopConsumers() {
super.stopConsumers(); super.stopConsumers();

63
application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java

@ -458,7 +458,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
schedule = schedule.replace("\"enabled\":false", "\"enabled\":true"); schedule = schedule.replace("\"enabled\":false", "\"enabled\":true");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"schedule\":" + schedule + "}");
checkAlarmResult(calculatedField, alarmResult -> {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
// checking multiple debug events due to scheduled reevaluation (which also produces debug events)
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class);
assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -638,11 +645,11 @@ public class AlarmRulesTest extends AbstractControllerTest {
AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null) AlarmSeverity.CRITICAL, new Condition("return temperature >= 50 && humidity >= 50;", null, null)
); );
CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm", CalculatedField calculatedField = createAlarmCf(deviceId, "High Temperature and Humidity Alarm",
arguments, createRules, null); arguments, createRules, null, configuration -> {
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration(); configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
configuration.getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(""" "temperature is ${temperature}, humidity is ${humidity}"
temperature is ${temperature}, humidity is ${humidity}"""); );
calculatedField = saveCalculatedField(calculatedField); });
postTelemetry(deviceId, "{\"temperature\":50}"); postTelemetry(deviceId, "{\"temperature\":50}");
postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}"); postAttributes(deviceId, AttributeScope.SERVER_SCOPE, "{\"humidity\":50}");
@ -653,6 +660,18 @@ public class AlarmRulesTest extends AbstractControllerTest {
assertThat(alarmResult.getAlarm().getDetails().get("data").asText()) assertThat(alarmResult.getAlarm().getDetails().get("data").asText())
.isEqualTo("temperature is 50, humidity is 50"); .isEqualTo("temperature is 50, humidity is 50");
}); });
((AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration()).getCreateRules().get(AlarmSeverity.CRITICAL).setAlarmDetails(
"UPDATED temperature is ${temperature}, humidity is ${humidity}"
);
calculatedField = saveCalculatedField(calculatedField);
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isCreated()).isFalse();
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getDetails().get("data").asText())
.isEqualTo("UPDATED temperature is 50, humidity is 50");
});
} }
@Test @Test
@ -683,7 +702,12 @@ public class AlarmRulesTest extends AbstractControllerTest {
Thread.sleep(10000); Thread.sleep(10000);
assertThat(getLatestAlarmResult(calculatedField.getId())).isNull(); assertThat(getLatestAlarmResult(calculatedField.getId())).isNull();
checkAlarmResult(calculatedField, alarmResult -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
CalculatedFieldDebugEvent debugEvent = getDebugEvents(calculatedField.getId(), 5).stream()
.filter(event -> event.getResult() != null)
.findFirst().orElse(null);
assertThat(debugEvent).isNotNull();
TbAlarmResult alarmResult = JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class);
assertThat(alarmResult.isCreated()).isTrue(); assertThat(alarmResult.isCreated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL); assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK); assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
@ -764,24 +788,14 @@ public class AlarmRulesTest extends AbstractControllerTest {
String alarmType, String alarmType,
Map<String, Argument> arguments, Map<String, Argument> arguments,
Map<AlarmSeverity, Condition> createConditions, Map<AlarmSeverity, Condition> createConditions,
Condition clearCondition) { Condition clearCondition,
Consumer<AlarmCalculatedFieldConfiguration>... modifier) {
Map<AlarmSeverity, AlarmRule> createRules = new HashMap<>(); Map<AlarmSeverity, AlarmRule> createRules = new HashMap<>();
createConditions.forEach((severity, condition) -> { createConditions.forEach((severity, condition) -> {
createRules.put(severity, toAlarmRule(condition)); createRules.put(severity, toAlarmRule(condition));
}); });
AlarmRule clearRule = clearCondition != null ? toAlarmRule(clearCondition) : null; 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<String, Argument> arguments,
Map<AlarmSeverity, AlarmRule> createRules,
AlarmRule clearRule) {
CalculatedField calculatedField = new CalculatedField(); CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId); calculatedField.setEntityId(entityId);
calculatedField.setName(alarmType); calculatedField.setName(alarmType);
@ -792,7 +806,16 @@ public class AlarmRulesTest extends AbstractControllerTest {
configuration.setClearRule(clearRule); configuration.setClearRule(clearRule);
calculatedField.setConfiguration(configuration); calculatedField.setConfiguration(configuration);
calculatedField.setDebugSettings(DebugSettings.all()); calculatedField.setDebugSettings(DebugSettings.all());
return saveCalculatedField(calculatedField); if (modifier.length > 0) {
modifier[0].accept(configuration);
}
CalculatedField savedCalculatedField = saveCalculatedField(calculatedField);
CalculatedFieldDebugEvent debugEvent = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getDebugEvents(savedCalculatedField.getId(), 1),
events -> !events.isEmpty()).get(0);
latestEventId = debugEvent.getId();
return savedCalculatedField;
} }
private AlarmRule toAlarmRule(Condition condition) { private AlarmRule toAlarmRule(Condition condition) {

35
application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java

@ -17,10 +17,10 @@ package org.thingsboard.server.cf;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.test.annotation.DirtiesContext;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
@ -64,8 +64,8 @@ import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL; import static org.thingsboard.server.cf.CalculatedFieldIntegrationTest.POLL_INTERVAL;
@Slf4j
@DaoSqlTest @DaoSqlTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractControllerTest { public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractControllerTest {
private Tenant savedTenant; private Tenant savedTenant;
@ -79,10 +79,10 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
private AssetProfile assetProfile; private AssetProfile assetProfile;
private Asset asset; private Asset asset;
private long deduplicationInterval = 10; private final long deduplicationInterval = 5;
@Before @Before
public void beforeTest() throws Exception { public void beforeEach() throws Exception {
loginSysAdmin(); loginSysAdmin();
updateDefaultTenantProfileConfig(tenantProfileConfig -> { updateDefaultTenantProfileConfig(tenantProfileConfig -> {
@ -135,7 +135,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
createOccupancyCF(assetProfile.getId()); createOccupancyCF(assetProfile.getId());
await().alias("create CF and perform initial aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) await().alias("create CF and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of( verifyTelemetry(asset.getId(), Map.of(
@ -153,7 +153,9 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
postTelemetry(device3.getId(), "{\"occupied\":true}"); postTelemetry(device3.getId(), "{\"occupied\":true}");
await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) await().alias("update telemetry and perform aggregation")
.atLeast(deduplicationInterval / 2, TimeUnit.SECONDS)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset2.getId(), Map.of( verifyTelemetry(asset2.getId(), Map.of(
@ -200,7 +202,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
postTelemetry(device3.getId(), "{\"occupied\":false}"); postTelemetry(device3.getId(), "{\"occupied\":false}");
await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval * 2, TimeUnit.SECONDS) await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset2.getId(), Map.of( verifyTelemetry(asset2.getId(), Map.of(
@ -284,7 +286,9 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
postTelemetry(device1.getId(), "{\"occupied\":false}"); postTelemetry(device1.getId(), "{\"occupied\":false}");
await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) await().alias("update telemetry and perform aggregation")
.atLeast(deduplicationInterval / 2, TimeUnit.SECONDS)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of( verifyTelemetry(asset.getId(), Map.of(
@ -323,14 +327,15 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
postTelemetry(device1.getId(), "{\"occupied\":false}"); postTelemetry(device1.getId(), "{\"occupied\":false}");
await().alias("update telemetry -> no changes").atMost(deduplicationInterval / 2, TimeUnit.SECONDS) await().alias("update telemetry -> no changes").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(this::checkInitialCalculationValues); .untilAsserted(this::checkInitialCalculationValues);
postTelemetry(device2.getId(), "{\"occupied\":false}"); postTelemetry(device2.getId(), "{\"occupied\":false}");
await().alias("create CF and perform initial calculation").atMost(deduplicationInterval, TimeUnit.SECONDS) await().alias("create CF and perform initial calculation")
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .atLeast(deduplicationInterval / 2, TimeUnit.SECONDS)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of( verifyTelemetry(asset.getId(), Map.of(
"freeSpaces", "2", "freeSpaces", "2",
@ -517,7 +522,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
postTelemetry(device2.getId(), "{\"temperature\":19.6}"); postTelemetry(device2.getId(), "{\"temperature\":19.6}");
CalculatedField cf = createAvgTemperatureCF(asset.getId()); CalculatedField cf = createAvgTemperatureCF(asset.getId());
await().alias("create avg temp cf and perform initial aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) await().alias("create avg temp cf and perform initial aggregation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24")); verifyTelemetry(asset.getId(), Map.of("avgTemperature", "24"));
@ -531,7 +536,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
configuration.setMetrics(Map.of("maxTemperature", aggMetric)); configuration.setMetrics(Map.of("maxTemperature", aggMetric));
saveCalculatedField(cf); saveCalculatedField(cf);
await().alias("update metrics and perform aggregation").atMost(deduplicationInterval / 2, TimeUnit.SECONDS) await().alias("update metrics and perform aggregation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of("maxTemperature", "24")); verifyTelemetry(asset.getId(), Map.of("maxTemperature", "24"));
@ -540,7 +545,9 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr
postTelemetry(device1.getId(), "{\"temperature\":101.3}"); postTelemetry(device1.getId(), "{\"temperature\":101.3}");
postTelemetry(device2.getId(), "{\"temperature\":25.8}"); postTelemetry(device2.getId(), "{\"temperature\":25.8}");
await().alias("update telemetry and perform aggregation").atMost(deduplicationInterval, TimeUnit.SECONDS) await().alias("update telemetry and perform aggregation")
.atLeast(deduplicationInterval / 2, TimeUnit.SECONDS)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
verifyTelemetry(asset.getId(), Map.of("maxTemperature", "26")); verifyTelemetry(asset.getId(), Map.of("maxTemperature", "26"));

2
application/src/test/resources/logback-test.xml

@ -17,8 +17,6 @@
<logger name="org.eclipse.leshan" level="INFO"/> <logger name="org.eclipse.leshan" level="INFO"/>
<logger name="org.thingsboard.server.controller.AbstractWebTest" level="INFO"/> <logger name="org.thingsboard.server.controller.AbstractWebTest" level="INFO"/>
<logger name="org.thingsboard.server.service.script" level="INFO"/> <logger name="org.thingsboard.server.service.script" level="INFO"/>
<logger name="org.thingsboard.server.service.cf.ctx.state.alarm" level="TRACE"/>
<logger name="org.thingsboard.server.actors.calculatedField" level="TRACE"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors --> <!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.PostgresEdgeGrpcSession" level="OFF"/> <logger name="org.thingsboard.server.service.edge.rpc.PostgresEdgeGrpcSession" level="OFF"/>

4
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -51,10 +51,6 @@ import java.util.UUID;
public interface AlarmService extends EntityDaoService { public interface AlarmService extends EntityDaoService {
/*
* New API, since 3.5.
*/
/** /**
* Designed for atomic operations over active alarms. * Designed for atomic operations over active alarms.
* Only one active alarm may exist for the pair {originatorId, alarmType} * Only one active alarm may exist for the pair {originatorId, alarmType}

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/AlarmRule.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule; package org.thingsboard.server.common.data.alarm.rule;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
@ -30,6 +31,7 @@ public class AlarmRule {
private String alarmDetails; private String alarmDetails;
private DashboardId dashboardId; private DashboardId dashboardId;
@JsonIgnore
public boolean requiresScheduledReevaluation() { public boolean requiresScheduledReevaluation() {
return condition.hasSchedule(); return condition.hasSchedule();
} }

1
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmCondition.java

@ -45,6 +45,7 @@ public abstract class AlarmCondition {
@Valid @Valid
private AlarmConditionValue<AlarmSchedule> schedule; private AlarmConditionValue<AlarmSchedule> schedule;
@JsonIgnore
public boolean hasSchedule() { public boolean hasSchedule() {
return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule); return schedule != null && !(schedule.getStaticValue() instanceof AnyTimeSchedule);
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/AlarmConditionValue.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition; package org.thingsboard.server.common.data.alarm.rule.condition;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -27,4 +29,10 @@ public class AlarmConditionValue<T> {
private T staticValue; private T staticValue;
private String dynamicValueArgument; private String dynamicValueArgument;
@JsonIgnore
@AssertTrue(message = "Either staticValue or dynamicValueArgument must be set")
public boolean isValid() {
return staticValue != null ^ dynamicValueArgument != null;
}
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/AlarmConditionFilter.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression; package org.thingsboard.server.common.data.alarm.rule.condition.expression;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@ -24,7 +23,6 @@ import org.thingsboard.server.common.data.alarm.rule.condition.expression.predic
import java.io.Serializable; import java.io.Serializable;
@Schema
@Data @Data
public class AlarmConditionFilter implements Serializable { public class AlarmConditionFilter implements Serializable {

5
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/BooleanFilterPredicate.java

@ -15,13 +15,18 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
public class BooleanFilterPredicate implements SimpleKeyFilterPredicate<Boolean> { public class BooleanFilterPredicate implements SimpleKeyFilterPredicate<Boolean> {
@NotNull
private BooleanOperation operation; private BooleanOperation operation;
@Valid
@NotNull
private AlarmConditionValue<Boolean> value; private AlarmConditionValue<Boolean> value;
@Override @Override

5
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/NumericFilterPredicate.java

@ -15,13 +15,18 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> { public class NumericFilterPredicate implements SimpleKeyFilterPredicate<Double> {
@NotNull
private NumericOperation operation; private NumericOperation operation;
@Valid
@NotNull
private AlarmConditionValue<Double> value; private AlarmConditionValue<Double> value;
@Override @Override

6
common/data/src/main/java/org/thingsboard/server/common/data/alarm/rule/condition/expression/predicate/StringFilterPredicate.java

@ -15,13 +15,18 @@
*/ */
package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue;
@Data @Data
public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> { public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> {
@NotNull
private StringOperation operation; private StringOperation operation;
@Valid
@NotNull
private AlarmConditionValue<String> value; private AlarmConditionValue<String> value;
private boolean ignoreCase; private boolean ignoreCase;
@ -40,4 +45,5 @@ public class StringFilterPredicate implements SimpleKeyFilterPredicate<String> {
IN, IN,
NOT_IN NOT_IN
} }
} }

35
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AlarmCalculatedFieldConfiguration.java

@ -15,15 +15,24 @@
*/ */
package org.thingsboard.server.common.data.cf.configuration; package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; 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.AlarmRule;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.stream.Stream;
import static java.util.Map.Entry.comparingByKey;
@Data @Data
public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration { public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration {
@ -51,15 +60,31 @@ public class AlarmCalculatedFieldConfiguration implements ArgumentsBasedCalculat
return null; return null;
} }
@JsonIgnore
@Override @Override
public void validate() { public boolean requiresScheduledReevaluation() {
return getAllRules().anyMatch(entry -> entry.getValue().requiresScheduledReevaluation());
}
@JsonIgnore
public Stream<Pair<AlarmSeverity, AlarmRule>> getAllRules() {
Stream<Pair<AlarmSeverity, AlarmRule>> rules = createRules.entrySet().stream()
.map(entry -> Pair.of(entry.getKey(), entry.getValue()));
if (clearRule != null) {
rules = Stream.concat(rules, Stream.of(Pair.of(null, clearRule)));
}
return rules.sorted(comparingByKey(Comparator.nullsLast(Comparator.naturalOrder())));
} }
@Override public boolean rulesEqual(AlarmCalculatedFieldConfiguration other, BiPredicate<AlarmRule, AlarmRule> equalityCheck) {
public boolean requiresScheduledReevaluation() { List<Pair<AlarmSeverity, AlarmRule>> thisRules = this.getAllRules().toList();
return createRules.values().stream().anyMatch(AlarmRule::requiresScheduledReevaluation) || List<Pair<AlarmSeverity, AlarmRule>> otherRules = other.getAllRules().toList();
(clearRule != null && clearRule.requiresScheduledReevaluation()); return CollectionsUtil.elementsEqual(thisRules, otherRules, (thisRule, otherRule) -> {
if (!Objects.equals(thisRule.getKey(), otherRule.getKey())) {
return false;
}
return equalityCheck.test(thisRule.getValue(), otherRule.getValue());
});
} }
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java

@ -52,7 +52,7 @@ public interface CalculatedFieldConfiguration {
Output getOutput(); Output getOutput();
void validate(); default void validate() {}
@JsonIgnore @JsonIgnore
default List<EntityId> getReferencedEntities() { default List<EntityId> getReferencedEntities() {
@ -74,6 +74,7 @@ public interface CalculatedFieldConfiguration {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@JsonIgnore
default boolean requiresScheduledReevaluation() { default boolean requiresScheduledReevaluation() {
return false; return false;
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/event/CalculatedFieldDebugEvent.java

@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import java.util.UUID; import java.util.UUID;
@ToString @ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class CalculatedFieldDebugEvent extends Event { public class CalculatedFieldDebugEvent extends Event {

29
common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java

@ -18,9 +18,11 @@ package org.thingsboard.server.common.data.util;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiPredicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class CollectionsUtil { public class CollectionsUtil {
@ -95,4 +97,31 @@ public class CollectionsUtil {
return false; return false;
} }
public static <T> boolean elementsEqual(Iterable<T> iterable1, Iterable<T> iterable2, BiPredicate<T, T> equalityCheck) {
if (iterable1 instanceof Collection<?> collection1 && iterable2 instanceof Collection<?> collection2) {
if (collection1.size() != collection2.size()) {
return false;
}
}
Iterator<T> iterator1 = iterable1.iterator();
Iterator<T> iterator2 = iterable2.iterator();
while (true) {
if (iterator1.hasNext()) {
if (!iterator2.hasNext()) {
return false;
}
T o1 = iterator1.next();
T o2 = iterator2.next();
if (equalityCheck.test(o1, o2)) {
continue;
} else {
return false;
}
}
return !iterator2.hasNext();
}
}
} }

2
common/proto/src/main/proto/queue.proto

@ -1728,12 +1728,10 @@ message ToCalculatedFieldMsg {
CalculatedFieldTelemetryMsgProto telemetryMsg = 1; CalculatedFieldTelemetryMsgProto telemetryMsg = 1;
CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2;
EntityActionEventProto eventMsg = 3; EntityActionEventProto eventMsg = 3;
repeated string cfTypes = 4;
} }
message ToCalculatedFieldNotificationMsg { message ToCalculatedFieldNotificationMsg {
CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1; CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1;
repeated string cfTypes = 2;
} }
/* Messages that are handled by ThingsBoard RuleEngine Service */ /* Messages that are handled by ThingsBoard RuleEngine Service */

2
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -159,7 +159,7 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
@Override @Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) { public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) {
var result = withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details)); var result = withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details));
if (result.getAlarm() != null) { if (pushEvent && result.getAlarm() != null) {
eventPublisher.publishEvent(ActionEntityEvent.builder() eventPublisher.publishEvent(ActionEntityEvent.builder()
.tenantId(tenantId) .tenantId(tenantId)
.entityId(result.getAlarm().getId()) .entityId(result.getAlarm().getId())

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -55,7 +55,6 @@ import static org.thingsboard.server.common.data.StringUtils.splitByCommaWithout
@Data @Data
@Slf4j @Slf4j
class AlarmRuleState { class AlarmRuleState {
private final AlarmSeverity severity; private final AlarmSeverity severity;

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java

@ -51,14 +51,14 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.ACTION, type = ComponentType.ACTION,
name = "device profile (deprecated)", // TODO: add description on why is it deprecated and what to use name = "device profile (deprecated)",
customRelations = true, customRelations = true,
relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"}, relationTypes = {"Alarm Created", "Alarm Updated", "Alarm Severity Updated", "Alarm Cleared", "Success", "Failure"},
version = 1, version = 1,
configClazz = TbDeviceProfileNodeConfiguration.class, configClazz = TbDeviceProfileNodeConfiguration.class,
nodeDescription = "Process device messages based on device profile settings (deprecated)", nodeDescription = "Process device messages based on device profile settings (deprecated)",
nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. The output relation type is either " + nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. The output relation type is either " +
"'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.",
configDirective = "tbActionNodeDeviceProfileConfig", configDirective = "tbActionNodeDeviceProfileConfig",
docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/" docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/"
) )

Loading…
Cancel
Save