Browse Source

Base implementation of Alarm rules CF

pull/14107/head
VIacheslavKlimov 1 year ago
parent
commit
3e11282d8f
  1. 30
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  2. 41
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java
  3. 57
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java
  4. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java
  5. 81
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  6. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java
  7. 3
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java
  8. 62
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java
  9. 2
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java
  10. 13
      application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java
  11. 10
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  12. 18
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java
  14. 80
      application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java
  15. 3
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java
  16. 5
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java
  17. 27
      application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java
  18. 48
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java
  19. 22
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java
  20. 31
      application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java
  21. 74
      application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java
  22. 39
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  23. 179
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  24. 25
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  25. 46
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java
  26. 40
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java
  27. 320
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  28. 6
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java
  29. 233
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  30. 39
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  31. 20
      application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java
  32. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java
  33. 75
      application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java
  34. 35
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java
  35. 7
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  36. 1
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  37. 13
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java
  38. 67
      application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java
  39. 191
      application/src/test/java/org/thingsboard/server/cf/AlarmRulesTest.java
  40. 23
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  41. 18
      application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
  42. 24
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  43. 2
      application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
  44. 48
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  45. 26
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java
  46. 28
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java
  47. 7
      application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java
  48. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  49. 2
      common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
  50. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java
  51. 7
      common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java
  52. 23
      common/proto/src/main/proto/queue.proto
  53. 14
      common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java
  54. 31
      common/util/src/main/java/org/thingsboard/common/util/KvUtil.java
  55. 24
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  56. 12
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  57. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java
  58. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java
  59. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java
  60. 17
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  61. 4
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java

30
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -43,7 +43,6 @@ import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
import java.util.HashSet;
import java.util.Optional;
@ -94,11 +93,14 @@ public class AppActor extends ContextAwareActor {
case COMPONENT_LIFE_CYCLE_MSG:
onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
break;
case CF_ENTITY_ACTION_EVENT_MSG:
forwardToTenantActor((TenantAwareMsg) msg, true);
break;
case QUEUE_TO_RULE_ENGINE_MSG:
onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg);
break;
case TRANSPORT_TO_DEVICE_ACTOR_MSG:
onToDeviceActorMsg((TenantAwareMsg) msg, false);
forwardToTenantActor((TenantAwareMsg) msg, false);
break;
case DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG:
case DEVICE_CREDENTIALS_UPDATE_TO_DEVICE_ACTOR_MSG:
@ -108,7 +110,7 @@ public class AppActor extends ContextAwareActor {
case DEVICE_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
case SERVER_RPC_RESPONSE_TO_DEVICE_ACTOR_MSG:
case REMOVE_RPC_TO_DEVICE_ACTOR_MSG:
onToDeviceActorMsg((TenantAwareMsg) msg, true);
forwardToTenantActor((TenantAwareMsg) msg, true);
break;
case SESSION_TIMEOUT_MSG:
ctx.broadcastToChildrenByType(msg, EntityType.TENANT);
@ -117,11 +119,11 @@ public class AppActor extends ContextAwareActor {
case CF_STATE_RESTORE_MSG:
//TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not.
// same for the Linked telemetry.
onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true);
forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, true);
break;
case CF_TELEMETRY_MSG:
case CF_LINKED_TELEMETRY_MSG:
onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false);
forwardToTenantActor((ToCalculatedFieldSystemMsg) msg, false);
break;
default:
return false;
@ -187,7 +189,7 @@ public class AppActor extends ContextAwareActor {
}
}
private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) {
private void forwardToTenantActor(TenantAwareMsg msg, boolean priority) {
getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> {
if (priority) {
tenantActor.tellWithHighPriority(msg);
@ -199,21 +201,6 @@ public class AppActor extends ContextAwareActor {
});
}
private void onToDeviceActorMsg(TenantAwareMsg msg, boolean priority) {
getOrCreateTenantActor(msg.getTenantId()).ifPresentOrElse(tenantActor -> {
if (priority) {
tenantActor.tellWithHighPriority(msg);
} else {
tenantActor.tell(msg);
}
}, () -> {
if (msg instanceof TransportToDeviceActorMsgWrapper) {
((TransportToDeviceActorMsgWrapper) msg).getCallback().onSuccess();
}
});
}
private Optional<TbActorRef> getOrCreateTenantActor(TenantId tenantId) {
if (deletedTenants.contains(tenantId)) {
return Optional.empty();
@ -245,6 +232,7 @@ public class AppActor extends ContextAwareActor {
public TbActor createActor() {
return new AppActor(context);
}
}
}

41
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldAlarmActionMsg.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.calculatedField;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
@Data
@Builder
public class CalculatedFieldAlarmActionMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
private final Alarm alarm;
private final ActionType action;
private final TbCallback callback;
@Override
public MsgType getMsgType() {
return MsgType.CF_ALARM_ACTION_MSG;
}
}

57
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActionEventMsg.java

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.actors.calculatedField;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto;
@Data
@Builder
public class CalculatedFieldEntityActionEventMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
private final EntityId entityId;
private final JsonNode entity;
private final ActionType action;
private final TbCallback callback;
public static CalculatedFieldEntityActionEventMsg fromProto(EntityActionEventProto proto,
TbCallback callback) {
return CalculatedFieldEntityActionEventMsg.builder()
.tenantId((TenantId) ProtoUtils.fromProto(proto.getTenantId()))
.entityId(ProtoUtils.fromProto(proto.getEntityId()))
.entity(JacksonUtil.toJsonNode(proto.getEntity()))
.action(ActionType.valueOf(proto.getAction()))
.callback(callback)
.build();
}
@Override
public MsgType getMsgType() {
return MsgType.CF_ENTITY_ACTION_EVENT_MSG;
}
}

3
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java

@ -78,6 +78,9 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor {
case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG:
processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg);
break;
case CF_ALARM_ACTION_MSG:
processor.process((CalculatedFieldAlarmActionMsg) msg);
break;
default:
return false;
}

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

@ -21,10 +21,12 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DebugModeUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
@ -48,6 +50,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import java.util.ArrayList;
@ -63,6 +66,7 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
/**
* @author Andrew Shvayka
@ -120,12 +124,23 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
public void process(EntityInitCalculatedFieldMsg msg) throws CalculatedFieldException {
log.debug("[{}] Processing entity init CF msg.", msg.getCtx().getCfId());
var ctx = msg.getCtx();
if (msg.isForceReinit()) {
log.debug("Force reinitialization of CF: [{}].", ctx.getCfId());
CalculatedFieldState state;
if (msg.getStateAction() == StateAction.RECREATE) {
states.remove(ctx.getCfId());
state = null;
} else {
state = states.get(ctx.getCfId());
}
try {
var state = getOrInitState(ctx);
if (state == null) {
state = createState(ctx);
} else if (msg.getStateAction() == StateAction.REINIT) {
log.debug("Force reinitialization of CF: [{}].", ctx.getCfId());
state.reset(ctx);
initState(state, ctx);
} else {
state.init(ctx);
}
if (state.isSizeOk()) {
processStateIfReady(ctx, Collections.singletonList(ctx.getCfId()), state, null, null, msg.getCallback());
} else {
@ -239,6 +254,19 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
msg.getCallback().onSuccess();
}
public void process(CalculatedFieldAlarmActionMsg msg) {
log.debug("[{}] Processing alarm action event msg: {}", entityId, msg);
states.values().forEach(state -> {
if (state instanceof AlarmCalculatedFieldState alarmCfState) {
Alarm stateAlarm = alarmCfState.getCurrentAlarm();
if (stateAlarm != null && stateAlarm.getId().equals(msg.getAlarm().getId())) {
alarmCfState.processAlarmAction(msg.getAlarm(), msg.getAction());
}
}
});
msg.getCallback().onSuccess();
}
private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List<CalculatedFieldId> cfIdList, MultipleTbCallback callback) throws CalculatedFieldException {
processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto));
}
@ -264,7 +292,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
CalculatedFieldState state = states.get(ctx.getCfId());
boolean justRestored = false;
if (state == null) {
state = getOrInitState(ctx);
state = createState(ctx);
justRestored = true;
} else if (state.isDirty()) {
log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId());
@ -277,7 +305,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
}
if (state.isSizeOk()) {
if (state.updateState(ctx, newArgValues) || justRestored) {
if (state.update(ctx, newArgValues) || justRestored) {
cfIdList = new ArrayList<>(cfIdList);
cfIdList.add(ctx.getCfId());
processStateIfReady(ctx, cfIdList, state, tbMsgId, tbMsgType, callback);
@ -289,30 +317,38 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
}
}
@SneakyThrows
private CalculatedFieldState getOrInitState(CalculatedFieldCtx ctx) {
CalculatedFieldState state = states.get(ctx.getCfId());
if (state != null) {
return state;
} else {
ListenableFuture<CalculatedFieldState> stateFuture = cfService.fetchStateFromDb(ctx, entityId);
// Ugly but necessary. We do not expect to often fetch data from DB. Only once per <Entity, CalculatedField> pair lifetime.
// This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low.
// Alternatively, we can fetch the state outside the actor system and push separate command to create this actor,
// but this will significantly complicate the code.
state = stateFuture.get(1, TimeUnit.MINUTES);
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
states.put(ctx.getCfId(), state);
}
private CalculatedFieldState createState(CalculatedFieldCtx ctx) {
CalculatedFieldState state = createStateByType(ctx, entityId);
initState(state, ctx);
return state;
}
private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) {
state.init(ctx);
Map<String, ArgumentEntry> arguments = fetchArguments(ctx);
state.update(ctx, arguments);
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
states.put(ctx.getCfId(), state);
}
@SneakyThrows
private Map<String, ArgumentEntry> fetchArguments(CalculatedFieldCtx ctx) {
ListenableFuture<Map<String, ArgumentEntry>> argumentsFuture = cfService.fetchArguments(ctx, entityId);
// Ugly but necessary. We do not expect to often fetch data from DB. Only once per <Entity, CalculatedField> pair lifetime.
// This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low.
// Alternatively, we can fetch the state outside the actor system and push separate command to create this actor,
// but this will significantly complicate the code.
return argumentsFuture.get(1, TimeUnit.MINUTES);
}
private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException {
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
boolean stateSizeChecked = false;
try {
if (ctx.isInitialized() && state.isReady()) {
CalculatedFieldResult calculationResult = state.performCalculation(entityId, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS);
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeChecked = true;
if (state.isSizeOk()) {
@ -322,13 +358,14 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
callback.onSuccess();
}
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.toStringOrElseNull(), null);
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.stringValue(), null);
}
}
} else {
callback.onSuccess();
}
} catch (Exception e) {
log.debug("[{}][{}] Failed to process CF state", entityId, ctx.getCfId(), e);
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
} finally {
if (!stateSizeChecked) {

3
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldLinkedTelemetryMsg.java

@ -22,7 +22,6 @@ import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
@Data
public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSystemMsg {
@ -32,9 +31,9 @@ public class CalculatedFieldLinkedTelemetryMsg implements ToCalculatedFieldSyste
private final CalculatedFieldLinkedTelemetryMsgProto proto;
private final TbCallback callback;
@Override
public MsgType getMsgType() {
return MsgType.CF_LINKED_TELEMETRY_MSG;
}
}

3
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java

@ -73,6 +73,9 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_ENTITY_LIFECYCLE_MSG:
processor.onEntityLifecycleMsg((CalculatedFieldEntityLifecycleMsg) msg);
break;
case CF_ENTITY_ACTION_EVENT_MSG:
processor.onEntityActionEventMsg((CalculatedFieldEntityActionEventMsg) msg);
break;
case CF_TELEMETRY_MSG:
processor.onTelemetryMsg((CalculatedFieldTelemetryMsg) msg);
break;

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

@ -16,15 +16,18 @@
package org.thingsboard.server.actors.calculatedField;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.TbActorCtx;
import org.thingsboard.server.actors.TbActorRef;
import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId;
import org.thingsboard.server.actors.calculatedField.EntityInitCalculatedFieldMsg.StateAction;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ProfileEntityIdInfo;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration;
@ -127,11 +130,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) {
var cfId = msg.getId().cfId();
var calculatedField = calculatedFields.get(cfId);
var ctx = calculatedFields.get(cfId);
if (calculatedField != null) {
if (ctx != null) {
if (msg.getState() != null) {
msg.getState().setRequiredArguments(calculatedField.getArgNames());
msg.getState().init(ctx);
}
log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId());
getOrCreateActor(msg.getId().entityId()).tell(msg);
@ -198,6 +201,22 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
}
public void onEntityActionEventMsg(CalculatedFieldEntityActionEventMsg msg) {
switch (msg.getAction()) {
case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> {
Alarm alarm = JacksonUtil.treeToValue(msg.getEntity(), Alarm.class);
CalculatedFieldAlarmActionMsg alarmActionMsg = CalculatedFieldAlarmActionMsg.builder()
.tenantId(tenantId)
.alarm(alarm)
.action(msg.getAction())
.callback(msg.getCallback())
.build();
getOrCreateActor(alarm.getOriginator()).tellWithHighPriority(alarmActionMsg);
}
default -> msg.getCallback().onSuccess();
}
}
private void onProfileDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
entityProfileCache.removeProfileId(msg.getEntityId());
callback.onSuccess();
@ -217,8 +236,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
var fieldsCount = entityIdFields.size() + profileIdFields.size();
if (fieldsCount > 0) {
MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback);
entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback));
profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback));
entityIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback));
profileIdFields.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback));
} else {
callback.onSuccess();
}
@ -237,7 +256,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
MultipleTbCallback multiCallback = new MultipleTbCallback(fieldsCount, callback);
var entityId = msg.getEntityId();
oldProfileCfs.forEach(ctx -> deleteCfForEntity(entityId, ctx.getCfId(), multiCallback));
newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, true, multiCallback));
newProfileCfs.forEach(ctx -> initCfForEntity(entityId, ctx, StateAction.INIT, multiCallback));
} else {
callback.onSuccess();
}
@ -275,13 +294,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb));
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, StateAction.INIT, cb));
}
}
}
private CalculatedFieldCtx getCfCtx(CalculatedField cf) {
return new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService());
return new CalculatedFieldCtx(cf, systemContext);
}
private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException {
@ -295,7 +314,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId);
callback.onSuccess();
} else {
var newCfCtx = getCfCtx(newCf);
var newCfCtx = getCfCtx(newCf); // fixme wtf? why isn't oldCfCtx closed properly? when to close it?
try {
newCfCtx.init();
} catch (Exception e) {
@ -328,21 +347,28 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
deleteLinks(oldCfCtx);
addLinks(newCf);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
var stateChanges = newCfCtx.hasStateChanges(oldCfCtx);
if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) {
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb));
StateAction stateAction;
if (newCfCtx.getCfType() != oldCfCtx.getCfType()) {
stateAction = StateAction.RECREATE;
} else if (newCfCtx.hasStateChanges(oldCfCtx)) {
stateAction = StateAction.REINIT;
} else if (newCfCtx.hasContextOnlyChanges(oldCfCtx)) {
stateAction = StateAction.REPROCESS;
} else {
callback.onSuccess();
return;
}
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateAction, cb));
}
}
}
private void onCfDeleted(ComponentLifecycleMsg msg, TbCallback callback) {
var cfId = new CalculatedFieldId(msg.getEntityId().getId());
var cfCtx = calculatedFields.remove(cfId);
var cfCtx = calculatedFields.remove(cfId); // fixme wtf? why isn't ctx closed properly?
if (cfCtx == null) {
log.debug("[{}] CF was already deleted [{}]", tenantId, cfId);
callback.onSuccess();
@ -489,9 +515,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback));
}
private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, boolean forceStateReinit, TbCallback callback) {
private void initCfForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, StateAction stateAction, TbCallback callback) {
log.debug("Pushing entity init CF msg to specific actor [{}]", entityId);
getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, callback, forceStateReinit));
getOrCreateActor(entityId).tell(new EntityInitCalculatedFieldMsg(tenantId, cfCtx, stateAction, callback));
}
private boolean isMyPartition(EntityId entityId, TbCallback callback) {
@ -555,7 +581,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
}
private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException {
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService());
var cfCtx = new CalculatedFieldCtx(cf, systemContext);
try {
cfCtx.init();
} catch (Exception e) {

2
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldTelemetryMsg.java

@ -31,9 +31,9 @@ public class CalculatedFieldTelemetryMsg implements ToCalculatedFieldSystemMsg {
private final CalculatedFieldTelemetryMsgProto proto;
private final TbCallback callback;
@Override
public MsgType getMsgType() {
return MsgType.CF_TELEMETRY_MSG;
}
}

13
application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityInitCalculatedFieldMsg.java

@ -16,26 +16,29 @@
package org.thingsboard.server.actors.calculatedField;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.List;
@Data
public class EntityInitCalculatedFieldMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
private final CalculatedFieldCtx ctx;
private final StateAction stateAction;
private final TbCallback callback;
private final boolean forceReinit;
@Override
public MsgType getMsgType() {
return MsgType.CF_ENTITY_INIT_CF_MSG;
}
public enum StateAction {
INIT,
REINIT,
RECREATE,
REPROCESS
}
}

10
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@ -155,6 +156,9 @@ public class TenantActor extends RuleChainManagerActor {
case COMPONENT_LIFE_CYCLE_MSG:
onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
break;
case CF_ENTITY_ACTION_EVENT_MSG:
forwardToCfActor((TenantAwareMsg) msg, true);
break;
case QUEUE_TO_RULE_ENGINE_MSG:
onQueueToRuleEngineMsg((QueueToRuleEngineMsg) msg);
break;
@ -182,11 +186,11 @@ public class TenantActor extends RuleChainManagerActor {
case CF_CACHE_INIT_MSG:
case CF_STATE_RESTORE_MSG:
case CF_PARTITIONS_CHANGE_MSG:
onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true);
forwardToCfActor((ToCalculatedFieldSystemMsg) msg, true);
break;
case CF_TELEMETRY_MSG:
case CF_LINKED_TELEMETRY_MSG:
onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, false);
forwardToCfActor((ToCalculatedFieldSystemMsg) msg, false);
break;
default:
return false;
@ -194,7 +198,7 @@ public class TenantActor extends RuleChainManagerActor {
return true;
}
private void onToCalculatedFieldSystemActorMsg(ToCalculatedFieldSystemMsg msg, boolean priority) {
private void forwardToCfActor(TenantAwareMsg msg, boolean priority) {
if (cfActor == null) {
if (msg instanceof CalculatedFieldStateRestoreMsg) {
log.warn("[{}] CF Actor is not initialized. ToCalculatedFieldSystemMsg: [{}]", tenantId, msg);

18
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java

@ -44,7 +44,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import java.util.HashMap;
import java.util.List;
@ -57,12 +56,11 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType;
import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument;
@Data
@Slf4j
public abstract class AbstractCalculatedFieldProcessingService {
public abstract class AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService {
protected final AttributesService attributesService;
protected final TimeseriesService timeseriesService;
@ -86,10 +84,11 @@ public abstract class AbstractCalculatedFieldProcessingService {
protected abstract String getExecutorNamePrefix();
public ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
@Override
public ListenableFuture<Map<String, ArgumentEntry>> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId) {
Map<String, ListenableFuture<ArgumentEntry>> argFutures = switch (ctx.getCalculatedField().getType()) {
case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false);
case SIMPLE, SCRIPT -> {
case SIMPLE, SCRIPT, ALARM -> {
Map<String, ListenableFuture<ArgumentEntry>> futures = new HashMap<>();
for (var entry : ctx.getArguments().entrySet()) {
var argEntityId = resolveEntityId(entityId, entry.getValue());
@ -99,11 +98,9 @@ public abstract class AbstractCalculatedFieldProcessingService {
yield futures;
}
};
return Futures.whenAllComplete(argFutures.values()).call(() -> {
var result = createStateByType(ctx);
result.updateState(ctx, resolveArgumentFutures(argFutures));
return result;
}, MoreExecutors.directExecutor());
return Futures.whenAllComplete(argFutures.values())
.call(() -> resolveArgumentFutures(argFutures),
MoreExecutors.directExecutor());
}
protected EntityId resolveEntityId(EntityId entityId, Argument argument) {
@ -174,6 +171,7 @@ public abstract class AbstractCalculatedFieldProcessingService {
yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)),
configuration::resolveEntityIds, calculatedFieldCallbackExecutor);
}
case CURRENT_CUSTOMER -> throw new UnsupportedOperationException(); // fixme implement
};
}

2
application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java

@ -64,7 +64,7 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF
protected void processRestoredState(CalculatedFieldStateProto stateMsg) {
var id = fromProto(stateMsg.getId());
var state = fromProto(stateMsg);
var state = fromProto(id, stateMsg);
processRestoredState(id, state);
}

80
application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbAlarmResult;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
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
public class AlarmCalculatedFieldResult implements CalculatedFieldResult {
private final TbAlarmResult alarmResult;
private final AlarmRuleState alarmRuleState;
@Override
public TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds) {
TbMsgMetaData metaData = new TbMsgMetaData();
if (alarmResult.isCreated()) {
metaData.putValue(DataConstants.IS_NEW_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isUpdated()) {
metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isSeverityUpdated()) {
metaData.putValue(DataConstants.IS_EXISTING_ALARM, Boolean.TRUE.toString());
metaData.putValue(DataConstants.IS_SEVERITY_UPDATED_ALARM, Boolean.TRUE.toString());
} 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()));
}
}
return TbMsg.newMsg()
.type(TbMsgType.ALARM)
.originator(entityId)
.data(JacksonUtil.toString(alarmResult.getAlarm()))
.metaData(metaData)
.build();
}
@Override
public String stringValue() {
return alarmResult != null ? JacksonUtil.toString(alarmResult) : null;
}
@Override
public boolean isEmpty() {
return alarmResult == null;
}
}

3
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldCache.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import java.util.List;
import java.util.function.Predicate;
public interface CalculatedFieldCache {
@ -36,6 +37,8 @@ public interface CalculatedFieldCache {
List<CalculatedFieldCtx> getCalculatedFieldCtxsByEntityId(EntityId entityId);
boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate<CalculatedFieldCtx> filter);
void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId);
void updateCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId);

5
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java

@ -25,20 +25,19 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import java.util.List;
import java.util.Map;
public interface CalculatedFieldProcessingService {
ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId);
ListenableFuture<Map<String, ArgumentEntry>> fetchArguments(CalculatedFieldCtx ctx, EntityId entityId);
Map<String, ArgumentEntry> fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId);
Map<String, ArgumentEntry> fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map<String, Argument> arguments);
void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List<CalculatedFieldId> cfIds, TbCallback callback);
void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback);
void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List<CalculatedFieldEntityCtxId> linkedCalculatedFields, TbCallback callback);

27
application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java

@ -15,27 +15,18 @@
*/
package org.thingsboard.server.service.cf;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.msg.TbMsg;
@Data
public final class CalculatedFieldResult {
import java.util.List;
private final OutputType type;
private final AttributeScope scope;
private final JsonNode result;
public interface CalculatedFieldResult {
public boolean isEmpty() {
return result == null || result.isMissingNode() || result.isNull() ||
(result.isObject() && result.isEmpty()) ||
(result.isArray() && result.isEmpty()) ||
(result.isTextual() && result.asText().isEmpty());
}
TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds);
public String toStringOrElseNull() {
return result == null ? null : result.toString();
}
String stringValue();
boolean isEmpty();
}

48
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java

@ -19,21 +19,24 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.List;
@ -42,6 +45,7 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
@Service
@Slf4j
@ -51,9 +55,10 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
private final ConcurrentReferenceHashMap<CalculatedFieldId, Lock> calculatedFieldFetchLocks = new ConcurrentReferenceHashMap<>();
private final CalculatedFieldService calculatedFieldService;
private final TbelInvokeService tbelInvokeService;
private final ApiLimitService apiLimitService;
private final RelationService relationService;
private final TbAssetProfileCache assetProfileCache;
private final TbDeviceProfileCache deviceProfileCache;
@Lazy
private final ActorSystemContext systemContext;
private final ConcurrentMap<CalculatedFieldId, CalculatedField> calculatedFields = new ConcurrentHashMap<>();
private final ConcurrentMap<EntityId, List<CalculatedField>> entityIdCalculatedFields = new ConcurrentHashMap<>();
@ -113,7 +118,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
if (ctx == null) {
CalculatedField calculatedField = getCalculatedField(calculatedFieldId);
if (calculatedField != null) {
ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService, relationService);
ctx = new CalculatedFieldCtx(calculatedField, systemContext);
calculatedFieldsCtx.put(calculatedFieldId, ctx);
log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx);
}
@ -136,6 +141,27 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
.toList();
}
@Override
public boolean hasCalculatedFields(TenantId tenantId, EntityId entityId, Predicate<CalculatedFieldCtx> filter) {
List<CalculatedFieldCtx> entityCfs = getCalculatedFieldCtxsByEntityId(entityId);
for (CalculatedFieldCtx ctx : entityCfs) {
if (filter.test(ctx)) {
return true;
}
}
EntityId profileId = getProfileId(tenantId, entityId);
if (profileId != null) {
List<CalculatedFieldCtx> profileCfs = getCalculatedFieldCtxsByEntityId(profileId);
for (CalculatedFieldCtx ctx : profileCfs) {
if (filter.test(ctx)) {
return true;
}
}
}
return false;
}
@Override
public void addCalculatedField(TenantId tenantId, CalculatedFieldId calculatedFieldId) {
Lock lock = getFetchLock(calculatedFieldId);
@ -185,6 +211,14 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
log.debug("[{}] evict calculated field links from cached links by entity id: {}", calculatedFieldId, oldCalculatedField);
}
private EntityId getProfileId(TenantId tenantId, EntityId entityId) {
return switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId();
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId();
default -> null;
};
}
private Lock getFetchLock(CalculatedFieldId id) {
return calculatedFieldFetchLocks.computeIfAbsent(id, __ -> new ReentrantLock());
}

22
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java

@ -25,13 +25,10 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
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.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -51,7 +48,6 @@ import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import java.util.ArrayList;
import java.util.HashMap;
@ -59,13 +55,12 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto;
@TbRuleEngineComponent
@Service
@Slf4j
public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService {
public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService {
private final TbClusterService clusterService;
private final PartitionService partitionService;
@ -86,11 +81,6 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
return "calculated-field-callback";
}
@Override
public ListenableFuture<CalculatedFieldState> fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
return super.fetchStateFromDb(ctx, entityId);
}
@Override
public Map<String, ArgumentEntry> fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) {
// only geofencing calculated fields supports dynamic arguments scheduled updates
@ -115,12 +105,9 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
}
@Override
public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculatedFieldResult, List<CalculatedFieldId> cfIds, TbCallback callback) {
public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List<CalculatedFieldId> cfIds, TbCallback callback) {
try {
OutputType type = calculatedFieldResult.getType();
TbMsgType msgType = OutputType.ATTRIBUTES.equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST;
TbMsgMetaData md = OutputType.ATTRIBUTES.equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY;
TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.toStringOrElseNull()).build();
TbMsg msg = result.toTbMsg(entityId, cfIds);
clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
@ -134,7 +121,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
}
});
} catch (Exception e) {
log.warn("[{}][{}] Failed to push message to rule engine. CalculatedFieldResult: {}", tenantId, entityId, calculatedFieldResult, e);
log.warn("[{}][{}] Failed to push message to rule engine. CalculatedFieldResult: {}", tenantId, entityId, result, e);
callback.onFailure(e);
}
}
@ -208,6 +195,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF
public void onFailure(Throwable t) {
callback.onFailure(t);
}
}
}

31
application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java

@ -26,9 +26,7 @@ import org.thingsboard.rule.engine.api.TimeseriesSaveRequest;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
@ -45,8 +43,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.Collections;
import java.util.EnumSet;
@ -74,8 +70,6 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
}
};
private final TbAssetProfileCache assetProfileCache;
private final TbDeviceProfileCache deviceProfileCache;
private final CalculatedFieldCache calculatedFieldCache;
private final TbClusterService clusterService;
@ -158,21 +152,9 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
if (!supportedReferencedEntities.contains(entityId.getEntityType())) {
return false;
}
List<CalculatedFieldCtx> entityCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(entityId);
for (CalculatedFieldCtx ctx : entityCfs) {
if (filter.test(ctx)) {
return true;
}
}
EntityId profileId = getProfileId(tenantId, entityId);
if (profileId != null) {
List<CalculatedFieldCtx> profileCfs = calculatedFieldCache.getCalculatedFieldCtxsByEntityId(profileId);
for (CalculatedFieldCtx ctx : profileCfs) {
if (filter.test(ctx)) {
return true;
}
}
if (calculatedFieldCache.hasCalculatedFields(tenantId, entityId, filter)) {
return true;
}
List<CalculatedFieldLink> links = calculatedFieldCache.getCalculatedFieldLinksByEntityId(entityId);
@ -186,14 +168,6 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
return false;
}
private EntityId getProfileId(TenantId tenantId, EntityId entityId) {
return switch (entityId.getEntityType()) {
case ASSET -> assetProfileCache.get(tenantId, (AssetId) entityId).getId();
case DEVICE -> deviceProfileCache.get(tenantId, (DeviceId) entityId).getId();
default -> null;
};
}
private ToCalculatedFieldMsg toCalculatedFieldTelemetryMsgProto(TimeseriesSaveRequest request, TimeseriesSaveResult result) {
CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = buildTelemetryMsgProto(request.getTenantId(), request.getEntityId(), request.getPreviousCalculatedFieldIds(), request.getTbMsgId(), request.getTbMsgType());
@ -305,6 +279,7 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS
public void onFailure(Throwable t) {
callback.onFailure(t);
}
}
}

74
application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java

@ -0,0 +1,74 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.configuration.OutputType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
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 java.util.List;
import java.util.Map;
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
@Data
@Builder
public final class TelemetryCalculatedFieldResult implements CalculatedFieldResult {
private final OutputType type;
private final AttributeScope scope;
private final JsonNode result;
@Override
public TbMsg toTbMsg(EntityId entityId, List<CalculatedFieldId> cfIds) {
TbMsgType msgType = switch (type) {
case ATTRIBUTES -> TbMsgType.POST_ATTRIBUTES_REQUEST;
case TIME_SERIES -> TbMsgType.POST_TELEMETRY_REQUEST;
};
TbMsgMetaData metaData = switch (type) {
case ATTRIBUTES -> new TbMsgMetaData(Map.of(SCOPE, scope.name()));
case TIME_SERIES -> TbMsgMetaData.EMPTY;
};
return TbMsg.newMsg()
.type(msgType)
.originator(entityId)
.previousCalculatedFieldIds(cfIds)
.data(stringValue())
.metaData(metaData)
.build();
}
@Override
public String stringValue() {
return result == null ? null : result.toString();
}
@Override
public boolean isEmpty() {
return result == null || result.isMissingNode() || result.isNull() ||
(result.isObject() && result.isEmpty()) ||
(result.isArray() && result.isEmpty()) ||
(result.isTextual() && result.asText().isEmpty());
}
}

39
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java

@ -15,41 +15,36 @@
*/
package org.thingsboard.server.service.cf.ctx.state;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@Getter
public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
protected final EntityId entityId;
protected List<String> requiredArguments;
protected Map<String, ArgumentEntry> arguments;
protected boolean sizeExceedsLimit;
protected Map<String, ArgumentEntry> arguments = new HashMap<>();
protected boolean sizeExceedsLimit;
protected long latestTimestamp = -1;
public BaseCalculatedFieldState(List<String> requiredArguments) {
this.requiredArguments = requiredArguments;
this.arguments = new HashMap<>();
public BaseCalculatedFieldState(EntityId entityId) {
this.entityId = entityId;
}
public BaseCalculatedFieldState() {
this(new ArrayList<>(), new HashMap<>(), false, -1);
@Override
public void init(CalculatedFieldCtx ctx) {
this.requiredArguments = ctx.getArgNames();
}
@Override
public boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) {
if (arguments == null) {
arguments = new HashMap<>();
}
public boolean update(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) {
boolean stateUpdated = false;
for (Map.Entry<String, ArgumentEntry> entry : argumentValues.entrySet()) {
@ -79,10 +74,18 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState {
return stateUpdated;
}
@Override
public void reset(CalculatedFieldCtx ctx) { // must reset everything dependent on arguments
requiredArguments = null;
arguments.clear();
sizeExceedsLimit = false;
latestTimestamp = -1;
}
@Override
public boolean isReady() {
return arguments.keySet().containsAll(requiredArguments) &&
arguments.values().stream().noneMatch(ArgumentEntry::isEmpty);
arguments.values().stream().noneMatch(ArgumentEntry::isEmpty);
}
@Override

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

@ -15,14 +15,22 @@
*/
package org.thingsboard.server.service.cf.ctx.state;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import org.mvel2.MVEL;
import org.thingsboard.common.util.ExpressionUtils;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.alarm.rule.AlarmRule;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
@ -36,20 +44,22 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
import java.util.Objects;
import java.util.stream.Stream;
@Data
public class CalculatedFieldCtx {
@ -67,10 +77,13 @@ public class CalculatedFieldCtx {
private Output output;
private String expression;
private boolean useLatestTs;
private TbelInvokeService tbelInvokeService;
private RelationService relationService;
private CalculatedFieldScriptEngine calculatedFieldScriptEngine;
private ThreadLocal<Expression> customExpression;
private AlarmSubscriptionService alarmService;
private Map<String, CalculatedFieldScriptEngine> tbelExpressions;
private Map<String, ThreadLocal<Expression>> simpleExpressions;
private boolean initialized;
@ -81,7 +94,8 @@ public class CalculatedFieldCtx {
private List<String> mainEntityGeofencingArgumentNames;
private List<String> linkedEntityGeofencingArgumentNames;
public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) {
public CalculatedFieldCtx(CalculatedField calculatedField,
ActorSystemContext systemContext) {
this.calculatedField = calculatedField;
this.cfId = calculatedField.getId();
@ -126,19 +140,20 @@ public class CalculatedFieldCtx {
});
}
}
this.tbelInvokeService = tbelInvokeService;
this.relationService = relationService;
this.tbelInvokeService = systemContext.getTbelInvokeService();
this.relationService = systemContext.getRelationService();
this.alarmService = systemContext.getAlarmService();
this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg);
this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024;
this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024;
this.maxDataPointsPerRollingArg = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); // fixme why tenant profile update is not handled??
this.maxStateSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024;
this.maxSingleValueArgumentSize = systemContext.getApiLimitService().getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024;
}
public void init() {
switch (cfType) {
case SCRIPT -> {
try {
this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService);
initTbelExpression(expression);
initialized = true;
} catch (Exception e) {
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e);
@ -146,28 +161,89 @@ public class CalculatedFieldCtx {
}
case GEOFENCING -> initialized = true;
case SIMPLE -> {
if (isValidExpression(expression)) {
this.customExpression = ThreadLocal.withInitial(() ->
new ExpressionBuilder(expression)
.functions(userDefinedFunctions)
.implicitMultiplication(true)
.variables(this.arguments.keySet())
.build()
);
initialized = true;
} else {
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.");
initSimpleExpression(expression);
initialized = true;
}
case ALARM -> {
AlarmCalculatedFieldConfiguration configuration = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
Stream<AlarmRule> rules = configuration.getCreateRules().values().stream();
if (configuration.getClearRule() != null) {
rules = Stream.concat(rules, Stream.of(configuration.getClearRule()));
}
rules.map(rule -> rule.getCondition().getExpression()).forEach(expression -> {
if (expression instanceof TbelAlarmConditionExpression tbelExpression) {
initTbelExpression(tbelExpression.getExpression());
}
});
initialized = true;
}
}
}
public void stop() {
if (calculatedFieldScriptEngine != null) {
calculatedFieldScriptEngine.destroy();
public double evaluateSimpleExpression(String expressionStr, CalculatedFieldState state) {
Expression expression = simpleExpressions.get(expressionStr).get();
for (Map.Entry<String, ArgumentEntry> entry : state.getArguments().entrySet()) {
try {
BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue();
double value = switch (kvEntry.getDataType()) {
case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow();
case DOUBLE -> kvEntry.getDoubleValue().orElseThrow();
case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow();
case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString());
};
expression.setVariable(entry.getKey(), value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number.");
}
}
return expression.evaluate();
}
public ListenableFuture<Object> evaluateTbelExpression(String expression, CalculatedFieldState state) {
Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
List<Object> args = new ArrayList<>(argNames.size() + 1);
args.add(new Object()); // first element is a ctx, but we will set it later;
for (String argName : argNames) {
var arg = toTbelArgument(argName, state);
arguments.put(argName, arg);
if (arg instanceof TbelCfSingleValueArg svArg) {
args.add(svArg.getValue());
} else {
args.add(arg);
}
}
args.set(0, new TbelCfCtx(arguments, state.getLatestTimestamp()));
return tbelExpressions.get(expression).executeScriptAsync(args.toArray());
}
private TbelCfArg toTbelArgument(String key, CalculatedFieldState state) {
return state.getArguments().get(key).toTbelCfArg();
}
private void initTbelExpression(String expression) {
if (tbelExpressions == null) {
tbelExpressions = new HashMap<>();
} else if (tbelExpressions.containsKey(expression)) {
return;
}
if (customExpression != null) {
customExpression.remove();
CalculatedFieldScriptEngine engine = initEngine(tenantId, expression, tbelInvokeService);
tbelExpressions.put(expression, engine);
}
private void initSimpleExpression(String expression) {
if (simpleExpressions == null) {
simpleExpressions = new HashMap<>();
} else if (simpleExpressions.containsKey(expression)) {
return;
}
if (isValidExpression(expression)) {
ThreadLocal<Expression> compiledExpression = ThreadLocal.withInitial(() ->
ExpressionUtils.createExpression(expression, this.arguments.keySet())
);
simpleExpressions.put(expression, compiledExpression);
} else {
throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.");
}
}
@ -326,21 +402,39 @@ public class CalculatedFieldCtx {
return new CalculatedFieldEntityCtxId(tenantId, cfId, entityId);
}
public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) {
boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression);
boolean outputChanged = !output.equals(other.output);
return expressionChanged || outputChanged;
public boolean hasContextOnlyChanges(CalculatedFieldCtx other) { // has changes that do not require state reinit and will be picked up by the state on the fly
if (calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression)) {
return true;
}
if (!output.equals(other.output)) {
return true;
}
if (cfType == CalculatedFieldType.ALARM && !calculatedField.getName().equals(other.getCalculatedField().getName())) {
return true;
}
return false;
}
public boolean hasStateChanges(CalculatedFieldCtx other) {
boolean typeChanged = !cfType.equals(other.cfType);
boolean argumentsChanged = !arguments.equals(other.arguments);
return typeChanged || argumentsChanged;
public boolean hasStateChanges(CalculatedFieldCtx other) { // has changes that require state reinit (will trigger state.reset() and re-fetch arguments)
boolean hasChanges = !arguments.equals(other.arguments);
if (hasChanges) {
return true;
}
if (cfType == CalculatedFieldType.ALARM) {
var thisConfig = (AlarmCalculatedFieldConfiguration) calculatedField.getConfiguration();
var otherConfig = (AlarmCalculatedFieldConfiguration) other.getCalculatedField().getConfiguration();
if (!thisConfig.getCreateRules().equals(otherConfig.getCreateRules()) ||
!Objects.equals(thisConfig.getClearRule(), otherConfig.getClearRule())) {
hasChanges = true;
}
// TODO: implement rules update logic!
}
return hasChanges;
}
public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) {
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) {
&& other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) {
boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled();
boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval();
return refreshTriggerChanged || refreshIntervalChanged;
@ -348,6 +442,15 @@ public class CalculatedFieldCtx {
return false;
}
public void stop() {
if (tbelExpressions != null) {
tbelExpressions.values().forEach(CalculatedFieldScriptEngine::destroy);
}
if (simpleExpressions != null) {
simpleExpressions.values().forEach(ThreadLocal::remove);
}
}
public String getSizeExceedsLimitMessage() {
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
}

25
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java

@ -17,29 +17,26 @@ package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.List;
import java.util.Map;
import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"),
@JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"),
@JsonSubTypes.Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"),
@Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"),
@Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"),
@Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"),
@Type(value = AlarmCalculatedFieldState.class, name = "ALARM")
})
public interface CalculatedFieldState {
@ -57,11 +54,13 @@ public interface CalculatedFieldState {
return false;
}
void setRequiredArguments(List<String> requiredArguments);
void init(CalculatedFieldCtx ctx);
boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues);
boolean update(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> arguments);
ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx);
void reset(CalculatedFieldCtx ctx);
ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx);
@JsonIgnore
boolean isReady();

46
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java

@ -15,35 +15,24 @@
*/
package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfCtx;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Data
@Slf4j
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
public ScriptCalculatedFieldState(List<String> requiredArguments) {
super(requiredArguments);
public ScriptCalculatedFieldState(EntityId entityId) {
super(entityId);
}
@Override
@ -52,30 +41,17 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
Map<String, TbelCfArg> arguments = new LinkedHashMap<>();
List<Object> args = new ArrayList<>(ctx.getArgNames().size() + 1);
args.add(new Object()); // first element is a ctx, but we will set it later;
for (String argName : ctx.getArgNames()) {
var arg = toTbelArgument(argName);
arguments.put(argName, arg);
if (arg instanceof TbelCfSingleValueArg svArg) {
args.add(svArg.getValue());
} else {
args.add(arg);
}
}
args.set(0, new TbelCfCtx(arguments, getLatestTimestamp()));
ListenableFuture<JsonNode> resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray());
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
ListenableFuture<Object> resultFuture = ctx.evaluateTbelExpression(ctx.getExpression(), this);
Output output = ctx.getOutput();
return Futures.transform(resultFuture,
result -> new CalculatedFieldResult(output.getType(), output.getScope(), result),
result -> TelemetryCalculatedFieldResult.builder()
.type(output.getType())
.scope(output.getScope())
.result(JacksonUtil.valueToTree(result))
.build(),
MoreExecutors.directExecutor()
);
}
private TbelCfArg toTbelArgument(String key) {
return arguments.get(key).toTbelCfArg();
}
}

40
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java

@ -19,27 +19,20 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbUtils;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.Output;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import java.util.List;
import java.util.Map;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
public SimpleCalculatedFieldState(List<String> requiredArguments) {
super(requiredArguments);
public SimpleCalculatedFieldState(EntityId entityId) {
super(entityId);
}
@Override
@ -55,31 +48,18 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
var expr = ctx.getCustomExpression().get();
for (Map.Entry<String, ArgumentEntry> entry : this.arguments.entrySet()) {
try {
BasicKvEntry kvEntry = ((SingleValueArgumentEntry) entry.getValue()).getKvEntryValue();
double value = switch (kvEntry.getDataType()) {
case LONG -> kvEntry.getLongValue().map(Long::doubleValue).orElseThrow();
case DOUBLE -> kvEntry.getDoubleValue().orElseThrow();
case BOOLEAN -> kvEntry.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElseThrow();
case STRING, JSON -> Double.parseDouble(kvEntry.getValueAsString());
};
expr.setVariable(entry.getKey(), value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Argument '" + entry.getKey() + "' is not a number.");
}
}
double expressionResult = expr.evaluate();
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
double expressionResult = ctx.evaluateSimpleExpression(ctx.getExpression(), this);
Output output = ctx.getOutput();
Object result = formatResult(expressionResult, output.getDecimalsByDefault());
JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result);
return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult));
return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder()
.type(output.getType())
.scope(output.getScope())
.result(outputResult)
.build());
}
private Object formatResult(double expressionResult, Integer decimals) {

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

@ -0,0 +1,320 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf.ctx.state.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbAlarmResult;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
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.expression.AlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.cf.AlarmCalculatedFieldResult;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
private String alarmType;
private AlarmCalculatedFieldConfiguration configuration;
@Getter
private final Map<AlarmSeverity, AlarmRuleState> createRuleStates = new TreeMap<>(Comparator.comparing(Enum::ordinal));
@Getter
private AlarmRuleState clearRuleState;
@Getter
private Alarm currentAlarm;
private boolean initialFetchDone;
public AlarmCalculatedFieldState(EntityId entityId) {
super(entityId);
}
@Override
public void init(CalculatedFieldCtx ctx) {
super.init(ctx);
this.alarmType = ctx.getCalculatedField().getName();
this.configuration = getConfiguration(ctx);
Map<AlarmSeverity, AlarmRule> createRules = configuration.getCreateRules();
createRules.forEach((severity, rule) -> {
AlarmRuleState ruleState = createRuleStates.get(severity);
if (ruleState == null) {
ruleState = new AlarmRuleState(severity, rule, this);
createRuleStates.put(severity, ruleState);
} else { // can be null if was restored
ruleState.setAlarmRule(rule);
// todo: is it enough to just set new alarm rule to alarm rule state? is it ok to leave the state as were??
}
});
createRuleStates.keySet().removeIf(severity -> !createRules.containsKey(severity));
AlarmRule clearRule = configuration.getClearRule();
if (clearRule != null) {
if (clearRuleState == null) {
clearRuleState = new AlarmRuleState(null, clearRule, this);
} else {
clearRuleState.setAlarmRule(clearRule);
}
} else {
clearRuleState = null;
}
log.debug("Initialized create rule states {} and clear rule state {} for {}", createRuleStates, clearRuleState, ctx.getCalculatedField());
}
@Override
public void reset(CalculatedFieldCtx ctx) {
super.reset(ctx);
}
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculatedFieldCtx ctx) {
initCurrentAlarm(ctx);
AlarmCalculatedFieldResult result = createOrClearAlarms(state -> state.eval(ctx), ctx);
return Futures.immediateFuture(result);
}
// TODO: harvesting
public ListenableFuture<CalculatedFieldResult> performCalculation(long ts, CalculatedFieldCtx ctx) {
initCurrentAlarm(ctx);
AlarmCalculatedFieldResult result = createOrClearAlarms(ruleState -> ruleState.eval(ts), ctx);
return Futures.immediateFuture(result);
}
@SneakyThrows
public boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) {
if (expression instanceof TbelAlarmConditionExpression tbelExpression) {
Object result = ctx.evaluateTbelExpression(tbelExpression.getExpression(), this).get();
if (result instanceof Boolean booleanResult) {
return booleanResult;
} else {
throw new IllegalStateException("Condition expression returned non-boolean value: '" + result + "'");
}
} else {
throw new UnsupportedOperationException("Simple expressions not supported");
}
}
public void processAlarmAction(Alarm alarm, ActionType action) {
switch (action) {
case ALARM_ACK -> processAlarmAck(alarm);
case ALARM_CLEAR -> processAlarmClear(alarm);
case ALARM_DELETE -> processAlarmDelete(alarm);
}
}
private void processAlarmClear(Alarm alarm) {
currentAlarm = null;
createRuleStates.values().forEach(AlarmRuleState::clear);
}
private void processAlarmAck(Alarm alarm) {
currentAlarm.setAcknowledged(alarm.isAcknowledged());
currentAlarm.setAckTs(alarm.getAckTs());
}
private void processAlarmDelete(Alarm alarm) {
currentAlarm = null;
createRuleStates.values().forEach(AlarmRuleState::clear);
}
public AlarmCalculatedFieldResult createOrClearAlarms(Function<AlarmRuleState, AlarmEvalResult> evalFunction, CalculatedFieldCtx ctx) {
TbAlarmResult result = null;
AlarmRuleState resultState = 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)) {
resultState = state;
break;
} else if (AlarmEvalResult.FALSE.equals(evalResult)) {
clearAlarmState(state);
}
}
if (resultState != null) {
result = calculateAlarmResult(resultState, ctx);
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)) {
clearAlarmState(clearRuleState);
for (AlarmRuleState state : createRuleStates.values()) {
clearAlarmState(state);
}
AlarmApiCallResult clearResult = ctx.getAlarmService().clearAlarm(
ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearRuleState), true
);
if (clearResult.isCleared()) {
result = new TbAlarmResult(false, false, true, clearResult.getAlarm());
resultState = clearRuleState;
}
currentAlarm = null;
} else if (AlarmEvalResult.FALSE.equals(evalResult)) {
clearAlarmState(clearRuleState);
}
}
return AlarmCalculatedFieldResult.builder()
.alarmResult(result)
.alarmRuleState(resultState)
.build();
}
private void clearAlarmState(AlarmRuleState state) {
if (state != null) {
state.clear();
}
}
private void initCurrentAlarm(CalculatedFieldCtx ctx) {
if (!initialFetchDone) {
Alarm alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), entityId, alarmType);
if (alarm != null && !alarm.getStatus().isCleared()) {
currentAlarm = alarm;
}
initialFetchDone = true;
}
}
private TbAlarmResult calculateAlarmResult(AlarmRuleState ruleState, CalculatedFieldCtx ctx) {
AlarmSeverity severity = ruleState.getSeverity();
if (currentAlarm != null) {
// TODO: In some extremely rare cases, we might miss the event of alarm clear (If one use in-mem queue and restarted the server) or (if one manipulated the rule chain).
// Maybe we should fetch alarm every time?
currentAlarm.setEndTs(System.currentTimeMillis());
AlarmSeverity oldSeverity = currentAlarm.getSeverity();
// Skip update if severity is decreased.
if (severity.ordinal() <= oldSeverity.ordinal()) {
currentAlarm.setDetails(createDetails(ruleState));
currentAlarm.setSeverity(severity);
AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
} else {
return null;
}
} else {
var newAlarm = new Alarm();
newAlarm.setType(alarmType);
newAlarm.setAcknowledged(false);
newAlarm.setCleared(false);
newAlarm.setSeverity(severity);
long startTs = latestTimestamp;
long currentTime = System.currentTimeMillis();
if (startTs == 0L || startTs > currentTime) {
startTs = currentTime;
}
newAlarm.setStartTs(startTs);
newAlarm.setEndTs(startTs);
newAlarm.setDetails(createDetails(ruleState));
newAlarm.setOriginator(entityId);
newAlarm.setTenantId(ctx.getTenantId());
newAlarm.setPropagate(configuration.isPropagate());
newAlarm.setPropagateToOwner(configuration.isPropagateToOwner());
newAlarm.setPropagateToTenant(configuration.isPropagateToTenant());
if (configuration.getPropagateRelationTypes() != null) {
newAlarm.setPropagateRelationTypes(configuration.getPropagateRelationTypes());
}
AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(newAlarm));
currentAlarm = result.getAlarm();
return TbAlarmResult.fromAlarmResult(result);
}
}
private JsonNode createDetails(AlarmRuleState ruleState) {
JsonNode alarmDetails;
String alarmDetailsStr = ruleState.getAlarmRule().getAlarmDetails();
DashboardId dashboardId = ruleState.getAlarmRule().getDashboardId();
if (StringUtils.isNotEmpty(alarmDetailsStr) || dashboardId != null) {
ObjectNode newDetails = JacksonUtil.newObjectNode();
if (StringUtils.isNotEmpty(alarmDetailsStr)) {
for (Map.Entry<String, ArgumentEntry> entry : arguments.entrySet()) {
String key = entry.getKey();
ArgumentEntry value = entry.getValue();
alarmDetailsStr = alarmDetailsStr.replaceAll(String.format("\\$\\{%s}", key), String.valueOf(value.getValue()));
}
newDetails.put("data", alarmDetailsStr);
}
if (dashboardId != null) {
newDetails.put("dashboardId", dashboardId.getId().toString());
}
alarmDetails = newDetails;
} else if (currentAlarm != null) {
alarmDetails = currentAlarm.getDetails();
} else {
alarmDetails = JacksonUtil.newObjectNode();
}
return alarmDetails;
}
protected SingleValueArgumentEntry getArgument(String key) {
SingleValueArgumentEntry entry = (SingleValueArgumentEntry) arguments.get(key);
if (entry == null) {
throw new IllegalArgumentException("Argument '" + key + "' is missing");
}
return entry;
}
private AlarmCalculatedFieldConfiguration getConfiguration(CalculatedFieldCtx ctx) {
return (AlarmCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
}
@Override
protected void validateNewEntry(ArgumentEntry newEntry) {
if (!(newEntry instanceof SingleValueArgumentEntry)) {
throw new IllegalArgumentException("Only single value arguments supported");
}
}
@Override
public CalculatedFieldType getType() {
return CalculatedFieldType.ALARM;
}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmStateUpdateResult.java → application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmEvalResult.java

@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.profile;
package org.thingsboard.server.service.cf.ctx.state.alarm;
enum AlarmStateUpdateResult {
public enum AlarmEvalResult {
NONE, CREATED, UPDATED, SEVERITY_UPDATED, CLEARED;
FALSE, NOT_YET_TRUE, TRUE;
}

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

@ -0,0 +1,233 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.cf.ctx.state.alarm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.KvUtil;
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.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.expression.AlarmConditionExpression;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.AlarmSchedule;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeSchedule;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.CustomTimeScheduleItem;
import org.thingsboard.server.common.data.alarm.rule.condition.schedule.SpecificTimeSchedule;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.function.Function;
@Data
@Slf4j
public class AlarmRuleState {
private final AlarmSeverity severity;
private AlarmRule alarmRule;
private AlarmCalculatedFieldState state;
private AlarmCondition condition;
private long lastEventTs;
private long duration;
private long eventCount;
public AlarmRuleState(AlarmSeverity severity, AlarmRule alarmRule, AlarmCalculatedFieldState state) {
this.severity = severity;
if (alarmRule != null) {
setAlarmRule(alarmRule);
}
this.state = state;
}
public AlarmEvalResult eval(CalculatedFieldCtx ctx) {
boolean active = isActive(state.getLatestTimestamp());
return switch (condition.getType()) {
case SIMPLE -> (active && eval(condition.getExpression(), ctx)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE;
case DURATION -> evalDuration(active, ctx);
case REPEATING -> evalRepeating(active, ctx);
};
}
public AlarmEvalResult eval(long ts) {
switch (condition.getType()) {
case SIMPLE:
case REPEATING:
return AlarmEvalResult.NOT_YET_TRUE;
case DURATION:
long requiredDurationInMs = getRequiredDurationInMs();
if (requiredDurationInMs > 0 && lastEventTs > 0 && ts > lastEventTs) {
long duration = this.duration + (ts - lastEventTs);
if (isActive(ts)) {
return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
} else {
return AlarmEvalResult.FALSE;
}
}
default:
return AlarmEvalResult.FALSE;
}
}
private boolean isActive(long eventTs) {
if (condition.getSchedule() == null) {
return true;
}
AlarmSchedule schedule = getValue(condition.getSchedule(), entry -> Optional.ofNullable(KvUtil.getStringValue(entry))
.map(str -> JsonConverter.parse(str, AlarmSchedule.class))
.orElse(null));
return switch (schedule.getType()) {
case ANY_TIME -> true;
case SPECIFIC_TIME -> isActiveSpecific((SpecificTimeSchedule) schedule, eventTs);
case CUSTOM -> isActiveCustom((CustomTimeSchedule) schedule, eventTs);
};
}
private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) {
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
if (schedule.getDaysOfWeek().size() != 7) {
int dayOfWeek = zdt.getDayOfWeek().getValue();
if (!schedule.getDaysOfWeek().contains(dayOfWeek)) {
return false;
}
}
long endsOn = schedule.getEndsOn();
if (endsOn == 0) {
// 24 hours in milliseconds
endsOn = 86400000;
}
return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), endsOn);
}
private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
int dayOfWeek = zdt.toLocalDate().getDayOfWeek().getValue();
for (CustomTimeScheduleItem item : schedule.getItems()) {
if (item.getDayOfWeek() == dayOfWeek) {
if (item.isEnabled()) {
long endsOn = item.getEndsOn();
if (endsOn == 0) {
// 24 hours in milliseconds
endsOn = 86400000;
}
return isActive(eventTs, zoneId, zdt, item.getStartsOn(), endsOn);
} else {
return false;
}
}
}
return false;
}
private boolean isActive(long eventTs, ZoneId zoneId, ZonedDateTime zdt, long startsOn, long endsOn) {
long startOfDay = zdt.toLocalDate().atStartOfDay(zoneId).toInstant().toEpochMilli();
long msFromStartOfDay = eventTs - startOfDay;
if (startsOn <= endsOn) {
return startsOn <= msFromStartOfDay && endsOn > msFromStartOfDay;
} else {
return startsOn < msFromStartOfDay || (0 < msFromStartOfDay && msFromStartOfDay < endsOn);
}
}
public void clear() {
eventCount = 0L;
lastEventTs = 0L;
duration = 0L;
}
private AlarmEvalResult evalRepeating(boolean active, CalculatedFieldCtx ctx) {
if (active && eval(condition.getExpression(), ctx)) {
eventCount++;
long requiredRepeats = getIntValue(((RepeatingAlarmCondition) condition).getCount());
return eventCount >= requiredRepeats ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
} else {
return AlarmEvalResult.FALSE;
}
}
private AlarmEvalResult evalDuration(boolean active, CalculatedFieldCtx ctx) {
if (active && eval(condition.getExpression(), ctx)) {
if (lastEventTs > 0) {
if (state.getLatestTimestamp() > lastEventTs) {
duration = duration + (state.getLatestTimestamp() - lastEventTs);
lastEventTs = state.getLatestTimestamp();
}
} else {
lastEventTs = state.getLatestTimestamp();
duration = 0L;
}
long requiredDurationInMs = getRequiredDurationInMs();
return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
} else {
return AlarmEvalResult.FALSE;
}
}
private Integer getIntValue(AlarmConditionValue<Integer> value) {
return getValue(value, entry -> Optional.ofNullable(KvUtil.getLongValue(entry)).map(Long::intValue).orElse(null));
}
private long getRequiredDurationInMs() {
return getValue(((DurationAlarmCondition) condition).getValue(), KvUtil::getLongValue);
}
private boolean eval(AlarmConditionExpression expression, CalculatedFieldCtx ctx) {
return state.eval(expression, ctx);
}
private <T> T getValue(AlarmConditionValue<T> conditionValue, Function<KvEntry, T> mapper) {
T value = conditionValue.getStaticValue();
if (value == null) {
String argument = conditionValue.getDynamicValueArgument();
SingleValueArgumentEntry entry = state.getArgument(argument);
value = mapper.apply(entry.getKvEntryValue());
if (value == null) {
throw new IllegalArgumentException("No value found for argument " + argument);
}
}
return value;
}
public void setAlarmRule(AlarmRule alarmRule) {
this.alarmRule = alarmRule;
this.condition = alarmRule.getCondition();
}
@Override
public String toString() {
return "AlarmRuleState{" +
"severity=" + severity +
", condition=" + condition +
", lastEventTs=" + lastEventTs +
", duration=" + duration +
", eventCount=" + eventCount +
'}';
}
}

39
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java

@ -19,8 +19,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.geo.Coordinates;
@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupC
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType;
import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState;
@ -39,7 +41,6 @@ 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -49,20 +50,16 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Ent
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE;
@Data
@Getter
@Setter
@Slf4j
@EqualsAndHashCode(callSuper = true)
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
private boolean dirty;
private boolean dirty = false;
public GeofencingCalculatedFieldState() {
super(new ArrayList<>(), new HashMap<>(), false, -1);
this.dirty = false;
}
public GeofencingCalculatedFieldState(List<String> argNames) {
super(argNames);
public GeofencingCalculatedFieldState(EntityId entityId) {
super(entityId);
}
@Override
@ -71,11 +68,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
public boolean updateState(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) {
if (arguments == null) {
arguments = new HashMap<>();
}
public boolean update(CalculatedFieldCtx ctx, Map<String, ArgumentEntry> argumentValues) {
boolean stateUpdated = false;
for (var entry : argumentValues.entrySet()) {
@ -117,7 +110,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
}
@Override
public ListenableFuture<CalculatedFieldResult> performCalculation(EntityId entityId, CalculatedFieldCtx ctx) {
public ListenableFuture<CalculatedFieldResult> performCalculation(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);
@ -157,13 +150,23 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode);
});
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode);
var result = TelemetryCalculatedFieldResult.builder()
.type(ctx.getOutput().getType())
.scope(ctx.getOutput().getScope())
.result(resultNode)
.build();
if (relationFutures.isEmpty()) {
return Futures.immediateFuture(result);
}
return Futures.whenAllComplete(relationFutures).call(() -> result, MoreExecutors.directExecutor());
}
@Override
public void reset(CalculatedFieldCtx ctx) {
super.reset(ctx);
dirty = false;
}
private Map<String, GeofencingArgumentEntry> getGeofencingArguments() {
return arguments.entrySet()
.stream()

20
application/src/main/java/org/thingsboard/server/service/edge/RelatedEdgesSourcingListener.java

@ -54,16 +54,18 @@ public class RelatedEdgesSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(ActionEntityEvent<?> event) {
executorService.submit(() -> {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
try {
switch (event.getActionType()) {
case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
}
} catch (Exception e) {
log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e);
switch (event.getActionType()) {
case ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE -> {
executorService.submit(() -> {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
try {
relatedEdgesService.publishRelatedEdgeIdsEvictEvent(event.getTenantId(), event.getEntityId());
} catch (Exception e) {
log.error("[{}] failed to process ActionEntityEvent: {}", event.getTenantId(), event, e);
}
});
}
});
}
}
@TransactionalEventListener(

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java

@ -77,7 +77,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor {
case ALARM_CLEAR_RPC_MESSAGE:
Alarm alarmToClear = edgeCtx.getAlarmService().findAlarmById(tenantId, alarmId);
if (alarmToClear != null) {
edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails());
edgeCtx.getAlarmService().clearAlarm(tenantId, alarmId, alarm.getClearTs(), alarm.getDetails(), true);
}
break;
case ENTITY_DELETED_RPC_MESSAGE:

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

@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Device;
@ -31,9 +32,11 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.DeviceId;
@ -53,13 +56,17 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.dao.edge.EdgeSynchronizationManager;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.gen.transport.TransportProtos.EntityActionEventProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.rule.engine.api.JobManager;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.cf.CalculatedFieldCache;
import java.util.Set;
@ -72,6 +79,7 @@ public class EntityStateSourcingListener {
private final TbClusterService tbClusterService;
private final EdgeSynchronizationManager edgeSynchronizationManager;
private final JobManager jobManager;
private final CalculatedFieldCache calculatedFieldCache;
@PostConstruct
public void init() {
@ -153,7 +161,7 @@ public class EntityStateSourcingListener {
return;
}
EntityType entityType = entityId.getEntityType();
if (!tenantId.isSysTenantId() && entityType != EntityType.TENANT && !tenantService.tenantExists(tenantId)) {
if (entityType != EntityType.TENANT && !tenantExists(tenantId)) {
log.debug("[{}] Ignoring DeleteEntityEvent because tenant does not exist: {}", tenantId, event);
return;
}
@ -216,18 +224,46 @@ public class EntityStateSourcingListener {
@TransactionalEventListener(fallbackExecution = true)
public void handleEvent(ActionEntityEvent<?> event) {
log.trace("[{}] ActionEntityEvent called: {}", event.getTenantId(), event);
if (ActionType.CREDENTIALS_UPDATED.equals(event.getActionType()) &&
EntityType.DEVICE.equals(event.getEntityId().getEntityType())
&& event.getEntity() instanceof DeviceCredentials) {
tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(event.getTenantId(),
(DeviceId) event.getEntityId(), (DeviceCredentials) event.getEntity()), null);
} else if (ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType()) && event.getEntity() instanceof Device device) {
Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class);
if (tenant != null) {
tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device);
TenantId tenantId = event.getTenantId();
log.trace("[{}] ActionEntityEvent called: {}", tenantId, event);
switch (event.getActionType()) {
case CREDENTIALS_UPDATED -> {
if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) &&
event.getEntity() instanceof DeviceCredentials deviceCredentials) {
tbClusterService.pushMsgToCore(new DeviceCredentialsUpdateNotificationMsg(tenantId,
(DeviceId) event.getEntityId(), deviceCredentials), null);
}
}
case ASSIGNED_TO_TENANT -> {
if (event.getEntity() instanceof Device device) {
Tenant tenant = JacksonUtil.fromString(event.getBody(), Tenant.class);
if (tenant != null) {
tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device);
}
pushAssignedFromNotification(tenant, tenantId, device);
}
}
case ALARM_ACK, ALARM_CLEAR, ALARM_DELETE -> {
if (event.getActionType() == ActionType.ALARM_DELETE && !tenantExists(tenantId)) {
return;
}
Alarm alarm = (Alarm) event.getEntity();
if (calculatedFieldCache.hasCalculatedFields(tenantId, alarm.getOriginator(), ctx -> ctx.getCfType() == CalculatedFieldType.ALARM)) {
ToCalculatedFieldMsg msg = ToCalculatedFieldMsg.newBuilder()
.setEventMsg(toProto(event))
.addCfTypes(CalculatedFieldType.ALARM.name())
.build();
tbClusterService.pushMsgToCalculatedFields(tenantId, alarm.getOriginator(), msg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {}
@Override
public void onFailure(Throwable t) {
log.error("[{}] Failed to push alarm event to CF queue: {}", tenantId, event, t);
}
});
}
}
pushAssignedFromNotification(tenant, event.getTenantId(), device);
}
}
@ -338,6 +374,10 @@ public class EntityStateSourcingListener {
}
}
private boolean tenantExists(TenantId tenantId) {
return tenantId.isSysTenantId() || tenantService.tenantExists(tenantId);
}
private TbMsgMetaData getMetaDataForAssignedFrom(Tenant tenant) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("assignedFromTenantId", tenant.getId().getId().toString());
@ -345,4 +385,13 @@ public class EntityStateSourcingListener {
return metaData;
}
private EntityActionEventProto toProto(ActionEntityEvent<?> event) {
return EntityActionEventProto.newBuilder()
.setTenantId(ProtoUtils.toProto(event.getTenantId()))
.setEntityId(ProtoUtils.toProto(event.getEntityId()))
.setAction(event.getActionType().name())
.setEntity(event.getEntity() != null ? JacksonUtil.toString(event.getEntity()) : "")
.build();
}
}

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

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.queue;
import com.google.protobuf.ProtocolStringList;
import jakarta.annotation.PreDestroy;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
@ -22,10 +23,12 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityActionEventMsg;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg;
import org.thingsboard.server.common.data.DataConstants;
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.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -57,6 +60,7 @@ import org.thingsboard.server.service.queue.processing.AbstractPartitionBasedCon
import org.thingsboard.server.service.queue.processing.IdMsgPair;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@ -158,12 +162,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
try {
ToCalculatedFieldMsg toCfMsg = msg.getValue();
pendingMsgHolder.setMsg(toCfMsg);
if (toCfMsg.hasTelemetryMsg()) {
log.trace("[{}] Forwarding regular telemetry message for processing {}", id, toCfMsg.getTelemetryMsg());
forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback);
} else if (toCfMsg.hasLinkedTelemetryMsg()) {
forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback);
}
processMsg(toCfMsg, id, callback);
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
callback.onFailure(e);
@ -181,6 +180,18 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
consumer.commit();
}
private void processMsg(ToCalculatedFieldMsg toCfMsg, UUID id, TbCallback callback) {
Set<CalculatedFieldType> cfTypes = getCfTypes(toCfMsg.getCfTypesList());
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());
forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback);
} else if (toCfMsg.hasLinkedTelemetryMsg()) {
forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback);
} else if (toCfMsg.hasEventMsg()) {
actorContext.tell(CalculatedFieldEntityActionEventMsg.fromProto(toCfMsg.getEventMsg(), callback));
}
}
@Override
protected ServiceType getServiceType() {
return ServiceType.TB_RULE_ENGINE;
@ -251,6 +262,18 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa
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
protected void stopConsumers() {
super.stopConsumers();

7
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -102,7 +102,12 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
@Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) {
return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details));
return clearAlarm(tenantId, alarmId, clearTs, details, true);
}
@Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) {
return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details, pushEvent));
}
@Override

1
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -169,6 +169,7 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
addMainCallback(resultFuture, result -> {
if (strategy.processCalculatedFields()) {
// TODO: divide CFs and alarm rules processing
calculatedFieldQueueService.pushRequestToQueue(request, result, request.getCallback());
} else {
request.getCallback().onSuccess(null);

13
application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import org.apache.commons.lang3.math.NumberUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
@ -28,10 +29,11 @@ import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import java.util.Optional;
@ -64,11 +66,12 @@ public class CalculatedFieldArgumentUtils {
return new StringDataEntry(key, defaultValue);
}
public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) {
public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx, EntityId entityId) {
return switch (ctx.getCfType()) {
case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames());
case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames());
case GEOFENCING -> new GeofencingCalculatedFieldState(ctx.getArgNames());
case SIMPLE -> new SimpleCalculatedFieldState(entityId);
case SCRIPT -> new ScriptCalculatedFieldState(entityId);
case GEOFENCING -> new GeofencingCalculatedFieldState(entityId);
case ALARM -> new AlarmCalculatedFieldState(entityId);
};
}

67
application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java

@ -17,6 +17,7 @@ package org.thingsboard.server.utils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
@ -26,6 +27,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.util.KvProtoUtil;
import org.thingsboard.server.common.util.ProtoUtils;
import org.thingsboard.server.gen.transport.TransportProtos.AlarmRuleStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.AlarmStateProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto;
import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto;
@ -38,13 +41,15 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto;
import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState;
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmRuleState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState;
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState;
import java.util.Map;
import java.util.Optional;
@ -95,9 +100,27 @@ public class CalculatedFieldUtils {
builder.addGeofencingArguments(toGeofencingArgumentProto(argName, geofencingArgumentEntry));
}
});
if (state instanceof AlarmCalculatedFieldState alarmState) {
AlarmStateProto.Builder alarmStateProto = AlarmStateProto.newBuilder();
alarmState.getCreateRuleStates().forEach((severity, ruleState) -> {
alarmStateProto.addCreateRuleStates(toAlarmRuleStateProto(ruleState));
});
if (alarmState.getClearRuleState() != null) {
alarmStateProto.setClearRuleState(toAlarmRuleStateProto(alarmState.getClearRuleState()));
}
}
return builder.build();
}
private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) {
return AlarmRuleStateProto.newBuilder()
.setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse(""))
.setLastEventTs(ruleState.getLastEventTs())
.setDuration(ruleState.getDuration())
.setEventCount(ruleState.getEventCount())
.build();
}
public static SingleValueArgumentProto toSingleValueArgumentProto(String argName, SingleValueArgumentEntry entry) {
SingleValueArgumentProto.Builder builder = SingleValueArgumentProto.newBuilder()
.setArgName(argName);
@ -143,7 +166,7 @@ public class CalculatedFieldUtils {
return builder.build();
}
public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) {
public static CalculatedFieldState fromProto(CalculatedFieldEntityCtxId id, CalculatedFieldStateProto proto) {
if (StringUtils.isEmpty(proto.getType())) {
return null;
}
@ -151,22 +174,36 @@ public class CalculatedFieldUtils {
CalculatedFieldType type = CalculatedFieldType.valueOf(proto.getType());
CalculatedFieldState state = switch (type) {
case SIMPLE -> new SimpleCalculatedFieldState();
case SCRIPT -> new ScriptCalculatedFieldState();
case GEOFENCING -> new GeofencingCalculatedFieldState();
case SIMPLE -> new SimpleCalculatedFieldState(id.entityId());
case SCRIPT -> new ScriptCalculatedFieldState(id.entityId());
case GEOFENCING -> new GeofencingCalculatedFieldState(id.entityId());
case ALARM -> new AlarmCalculatedFieldState(id.entityId());
};
proto.getSingleValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto)));
if (CalculatedFieldType.SCRIPT.equals(type)) {
proto.getRollingValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto)));
}
if (CalculatedFieldType.GEOFENCING.equals(type)) {
proto.getGeofencingArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto)));
switch (type) {
case SCRIPT -> {
proto.getRollingValueArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto)));
}
case GEOFENCING -> {
proto.getGeofencingArgumentsList().forEach(argProto ->
state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto)));
}
case ALARM -> {
AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state;
AlarmStateProto alarmStateProto = proto.getAlarmState();
for (AlarmRuleStateProto ruleStateProto : alarmStateProto.getCreateRuleStatesList()) {
AlarmSeverity severity = StringUtils.isNotEmpty(ruleStateProto.getSeverity()) ? AlarmSeverity.valueOf(ruleStateProto.getSeverity()) : null;
AlarmRuleState ruleState = new AlarmRuleState(severity, null, alarmState);
ruleState.setLastEventTs(ruleStateProto.getLastEventTs());
ruleState.setDuration(ruleStateProto.getDuration());
ruleState.setEventCount(ruleStateProto.getEventCount());
alarmState.getCreateRuleStates().put(severity, ruleState);
}
}
}
return state;

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

@ -0,0 +1,191 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.cf;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.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.SimpleAlarmCondition;
import org.thingsboard.server.common.data.alarm.rule.condition.expression.TbelAlarmConditionExpression;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.AlarmCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.cf.configuration.Argument;
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.event.CalculatedFieldDebugEvent;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EventId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.event.EventDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
@DaoSqlTest
public class AlarmRulesTest extends AbstractControllerTest {
@MockitoSpyBean
private ActorSystemContext actorSystemContext;
@Autowired
private EventDao eventDao;
private DeviceId deviceId;
private EventId latestEventId;
@Before
public void beforeEach() throws Exception {
loginTenantAdmin();
Device device = createDevice("Device A", "aaa");
deviceId = device.getId();
}
@Test
public void testCreateAndSeverityUpdateAndClear() throws Exception {
Argument temperatureArgument = new Argument();
temperatureArgument.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null));
Map<String, Argument> arguments = Map.of(
"temperature", temperatureArgument
);
Map<AlarmSeverity, String> 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);
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);
});
postTelemetry(deviceId, "{\"temperature\":100}");
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isSeverityUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":101}");
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isUpdated()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.ACTIVE_UNACK);
});
postTelemetry(deviceId, "{\"temperature\":20}");
checkAlarmResult(calculatedField, alarmResult -> {
assertThat(alarmResult.isCleared()).isTrue();
assertThat(alarmResult.getAlarm().getSeverity()).isEqualTo(AlarmSeverity.CRITICAL);
assertThat(alarmResult.getAlarm().getStatus()).isEqualTo(AlarmStatus.CLEARED_UNACK);
});
}
private void checkAlarmResult(CalculatedField calculatedField, Consumer<TbAlarmResult> assertion) {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
TbAlarmResult alarmResult = getLatestAlarmResult(calculatedField.getId());
assertThat(alarmResult).isNotNull();
assertion.accept(alarmResult);
Alarm alarm = alarmResult.getAlarm();
assertThat(alarm.getOriginator()).isEqualTo(deviceId);
assertThat(alarm.getType()).isEqualTo(calculatedField.getName());
});
}
private TbAlarmResult getLatestAlarmResult(CalculatedFieldId calculatedFieldId) {
List<CalculatedFieldDebugEvent> debugEvents = getDebugEvents(calculatedFieldId, 1);
if (debugEvents.isEmpty()) {
return null;
}
CalculatedFieldDebugEvent debugEvent = debugEvents.get(0);
if (debugEvent.getError() != null) {
System.err.println("CF error: " + debugEvent.getError());
Assertions.fail();
}
if (debugEvent.getId().equals(latestEventId)) {
return null;
}
latestEventId = debugEvent.getId();
return JacksonUtil.fromString(debugEvent.getResult(), TbAlarmResult.class);
}
private CalculatedField createAlarmCf(EntityId entityId,
String alarmType,
Map<String, Argument> arguments,
Map<AlarmSeverity, String> createConditions,
String clearCondition) {
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));
calculatedField.setConfiguration(configuration);
calculatedField.setDebugSettings(DebugSettings.all());
return saveCalculatedField(calculatedField);
}
private AlarmRule toAlarmRule(String conditionExpression) {
if (conditionExpression == null) {
return null;
}
AlarmRule rule = new AlarmRule();
SimpleAlarmCondition condition = new SimpleAlarmCondition();
TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression();
expression.setExpression(conditionExpression);
condition.setExpression(expression);
rule.setCondition(condition);
return rule;
}
private List<CalculatedFieldDebugEvent> getDebugEvents(CalculatedFieldId calculatedFieldId, int limit) {
return eventDao.findLatestEvents(tenantId.getId(), calculatedFieldId.getId(), EventType.DEBUG_CALCULATED_FIELD, limit).stream()
.map(e -> (CalculatedFieldDebugEvent) e).toList();
}
}

23
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -75,7 +75,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
@Test
public void testSimpleCalculatedFieldWhenAllTelemetryPresent() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}"));
postTelemetry(testDevice.getId(), "{\"temperature\":25}");
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}"));
CalculatedField calculatedField = new CalculatedField();
@ -112,7 +112,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
postTelemetry(testDevice.getId(), "{\"temperature\":30}");
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -133,6 +133,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
.untilAsserted(() -> {
ArrayNode temperatureF = getServerAttributes(testDevice.getId(), "temperatureF");
assertThat(temperatureF).isNotNull();
assertThat(temperatureF.get(0)).isNotNull();
assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("86.0");
});
@ -197,7 +198,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue();
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
postTelemetry(testDevice.getId(), "{\"temperature\":30}");
await().alias("update telemetry -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -246,7 +247,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
postTelemetry(testDevice.getId(), "{\"temperature\":30}");
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -431,7 +432,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
@Test
public void testSimpleCalculatedFieldWhenExpressionIsInvalid() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":25}"));
postTelemetry(testDevice.getId(), "{\"temperature\":25}");
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
@ -467,7 +468,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").isNull()).isTrue();
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"temperature\":30}"));
postTelemetry(testDevice.getId(), "{\"temperature\":30}");
await().alias("update telemetry -> ctx is not initialized -> no calculation perform").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -482,7 +483,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
public void testSimpleCalculatedFieldWhenUseLatestTsIsTrue() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis() - 300000L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)));
postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
@ -526,10 +527,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
long ts = System.currentTimeMillis();
long tsA = ts - 300000L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA)));
postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":1}}", tsA));
long tsB = ts - 300L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB)));
postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"b\":5}}", tsB));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());
@ -570,7 +571,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
long tsABeforeTsB = tsB - 300L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":10}}", tsABeforeTsB)));
postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"a\":10}}", tsABeforeTsB));
await().alias("update telemetry with ts less than latest -> save result with latest ts").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -586,7 +587,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
public void testScriptCalculatedFieldWhenUsedLatestTsInScript() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
long ts = System.currentTimeMillis() - 300000L;
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts)));
postTelemetry(testDevice.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":30}}", ts));
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(testDevice.getId());

18
application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java

@ -15,19 +15,13 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.dao.rule.RuleChainService;
@ -61,18 +55,6 @@ public abstract class AbstractRuleEngineControllerTest extends AbstractControlle
return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class);
}
protected PageData<EventInfo> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {
return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE.getOldName(), limit);
}
protected PageData<EventInfo> getEvents(TenantId tenantId, EntityId entityId, String eventType, int limit) throws Exception {
TimePageLink pageLink = new TimePageLink(limit);
return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
new TypeReference<PageData<EventInfo>>() {
}, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId());
}
protected JsonNode getMetadata(EventInfo outEvent) {
String metaDataStr = outEvent.getBody().get("metadata").asText();
try {

24
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -78,10 +78,12 @@ import org.thingsboard.server.actors.device.SessionInfo;
import org.thingsboard.server.actors.device.ToDeviceRpcRequestMetadata;
import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResourceInfo;
@ -89,6 +91,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
@ -101,6 +104,7 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration;
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.event.EventType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.CustomerId;
@ -1312,4 +1316,24 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
doPost("/api/job/" + jobId + "/reprocess").andExpect(status().isOk());
}
protected void postTelemetry(EntityId entityId, String payload) throws Exception {
doPost("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() +
"/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload)).andExpect(status().isOk());
}
protected CalculatedField saveCalculatedField(CalculatedField calculatedField) {
return doPost("/api/calculatedField", calculatedField, CalculatedField.class);
}
protected PageData<EventInfo> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {
return getEvents(tenantId, entityId, EventType.DEBUG_RULE_NODE, limit);
}
protected PageData<EventInfo> getEvents(TenantId tenantId, EntityId entityId, EventType eventType, int limit) throws Exception {
TimePageLink pageLink = new TimePageLink(limit);
return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
new TypeReference<PageData<EventInfo>>() {
}, pageLink, entityId.getEntityType(), entityId.getId(), eventType, tenantId.getId());
}
}

2
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java

@ -118,7 +118,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
.pollInterval(10, MILLISECONDS)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> {
List<EventInfo> debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT.getOldName(), 1000)
List<EventInfo> debugEvents = getEvents(tenantId, ruleChainFinal.getFirstRuleNodeId(), EventType.LC_EVENT, 1000)
.getData().stream().filter(e -> {
var body = e.getBody();
return body.has("event") && body.get("event").asText().equals("STARTED")

48
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java

@ -20,9 +20,11 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
@ -43,7 +45,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
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;
@ -91,13 +93,15 @@ public class GeofencingCalculatedFieldStateTest {
private ApiLimitService apiLimitService;
@Mock
private RelationService relationService;
@InjectMocks
private ActorSystemContext systemContext;
@BeforeEach
void setUp() {
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L);
ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, relationService);
ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext);
ctx.init();
state = new GeofencingCalculatedFieldState(ctx.getArgNames());
state = new GeofencingCalculatedFieldState(ctx.getEntityId());
}
@Test
@ -113,7 +117,7 @@ public class GeofencingCalculatedFieldStateTest {
));
Map<String, ArgumentEntry> newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry);
boolean stateUpdated = state.updateState(ctx, newArgs);
boolean stateUpdated = state.update(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@ -127,21 +131,21 @@ public class GeofencingCalculatedFieldStateTest {
@Test
void testUpdateStateWithInvalidArgumentTypeForLatitudeArgument() {
assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
assertThatThrownBy(() -> state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed.");
}
@Test
void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() {
assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
assertThatThrownBy(() -> state.update(ctx, Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed.");
}
@Test
void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() {
assertThatThrownBy(() -> state.updateState(ctx, Map.of("someArgumentName", latitudeArgEntry)))
assertThatThrownBy(() -> state.update(ctx, Map.of("someArgumentName", latitudeArgEntry)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for someArgumentName argument: SINGLE_VALUE. Only GEOFENCING type is allowed.");
}
@ -152,7 +156,7 @@ public class GeofencingCalculatedFieldStateTest {
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 190L);
Map<String, ArgumentEntry> newArgs = Map.of("latitude", newArgEntry);
boolean stateUpdated = state.updateState(ctx, newArgs);
boolean stateUpdated = state.update(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).isEqualTo(newArgs);
@ -164,7 +168,7 @@ public class GeofencingCalculatedFieldStateTest {
Map<String, ArgumentEntry> newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry);
boolean stateUpdated = state.updateState(ctx, newArgs);
boolean stateUpdated = state.update(ctx, newArgs);
assertThat(stateUpdated).isFalse();
assertThat(state.getArguments()).isEqualTo(newArgs);
@ -174,7 +178,7 @@ public class GeofencingCalculatedFieldStateTest {
void testUpdateStateWhenUpdateExistingSingleValueArgumentEntryWithValueOfAnotherType() {
state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry));
assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
assertThatThrownBy(() -> state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for single value argument entry: GEOFENCING");
}
@ -184,7 +188,7 @@ public class GeofencingCalculatedFieldStateTest {
void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithValueOfAnotherType() {
state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry));
assertThatThrownBy(() -> state.updateState(ctx, Map.of("allowedZones", latitudeArgEntry)))
assertThatThrownBy(() -> state.update(ctx, Map.of("allowedZones", latitudeArgEntry)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE");
}
@ -234,7 +238,7 @@ public class GeofencingCalculatedFieldStateTest {
when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
@ -250,9 +254,9 @@ 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result2 = performCalculation();
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
@ -309,7 +313,7 @@ public class GeofencingCalculatedFieldStateTest {
when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
@ -322,9 +326,9 @@ 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result2 = performCalculation();
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
@ -379,7 +383,7 @@ public class GeofencingCalculatedFieldStateTest {
when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true));
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
@ -394,9 +398,9 @@ 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.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
state.update(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude));
CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result2 = performCalculation();
assertThat(result2).isNotNull();
assertThat(result2.getType()).isEqualTo(output.getType());
@ -480,4 +484,8 @@ public class GeofencingCalculatedFieldStateTest {
return config;
}
private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException {
return (TelemetryCalculatedFieldResult) state.performCalculation(ctx).get();
}
}

26
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java

@ -18,12 +18,14 @@ package org.thingsboard.server.service.cf.ctx.state;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.DefaultTbelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -41,10 +43,9 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
@ -77,10 +78,15 @@ public class ScriptCalculatedFieldStateTest {
@BeforeEach
void setUp() {
ActorSystemContext systemContext = Mockito.mock(ActorSystemContext.class);
when(systemContext.getTbelInvokeService()).thenReturn(tbelInvokeService);
when(systemContext.getApiLimitService()).thenReturn(apiLimitService);
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L);
ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService, null);
ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext);
ctx.init();
state = new ScriptCalculatedFieldState(ctx.getArgNames());
state = new ScriptCalculatedFieldState(ctx.getEntityId());
state.init(ctx);
}
@Test
@ -93,7 +99,7 @@ public class ScriptCalculatedFieldStateTest {
state.arguments = new HashMap<>(Map.of("assetHumidity", assetHumidityArgEntry));
Map<String, ArgumentEntry> newArgs = Map.of("deviceTemperature", deviceTemperatureArgEntry);
boolean stateUpdated = state.updateState(ctx, newArgs);
boolean stateUpdated = state.update(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@ -110,7 +116,7 @@ public class ScriptCalculatedFieldStateTest {
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(ts, new LongDataEntry("assetHumidity", 41L), 349L);
Map<String, ArgumentEntry> newArgs = Map.of("assetHumidity", newArgEntry);
boolean stateUpdated = state.updateState(ctx, newArgs);
boolean stateUpdated = state.update(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@ -125,7 +131,7 @@ public class ScriptCalculatedFieldStateTest {
void testPerformCalculation() throws ExecutionException, InterruptedException {
state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry));
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
@ -141,7 +147,7 @@ public class ScriptCalculatedFieldStateTest {
"assetHumidity", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new LongDataEntry("a", 45L), 10L)
));
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
@ -221,4 +227,8 @@ public class ScriptCalculatedFieldStateTest {
return config;
}
private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException {
return (TelemetryCalculatedFieldResult) state.performCalculation(ctx).get();
}
}

28
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java

@ -18,9 +18,11 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -39,7 +41,7 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.service.cf.CalculatedFieldResult;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult;
import java.util.HashMap;
import java.util.Map;
@ -67,13 +69,15 @@ public class SimpleCalculatedFieldStateTest {
@Mock
private ApiLimitService apiLimitService;
@InjectMocks
private ActorSystemContext systemContext;
@BeforeEach
void setUp() {
when(apiLimitService.getLimit(any(), any())).thenReturn(1000L);
ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, null);
ctx = new CalculatedFieldCtx(getCalculatedField(), systemContext);
ctx.init();
state = new SimpleCalculatedFieldState(ctx.getArgNames());
state = new SimpleCalculatedFieldState(ctx.getEntityId());
}
@Test
@ -89,7 +93,7 @@ public class SimpleCalculatedFieldStateTest {
));
Map<String, ArgumentEntry> newArgs = Map.of("key3", key3ArgEntry);
boolean stateUpdated = state.updateState(ctx, newArgs);
boolean stateUpdated = state.update(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(
@ -107,7 +111,7 @@ public class SimpleCalculatedFieldStateTest {
SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new LongDataEntry("key1", 18L), 190L);
Map<String, ArgumentEntry> newArgs = Map.of("key1", newArgEntry);
boolean stateUpdated = state.updateState(ctx, newArgs);
boolean stateUpdated = state.update(ctx, newArgs);
assertThat(stateUpdated).isTrue();
assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf(Map.of("key1", newArgEntry));
@ -121,7 +125,7 @@ public class SimpleCalculatedFieldStateTest {
));
Map<String, ArgumentEntry> newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L));
assertThatThrownBy(() -> state.updateState(ctx, newArgs))
assertThatThrownBy(() -> state.update(ctx, newArgs))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Rolling argument entry is not supported for simple calculated fields.");
}
@ -134,7 +138,7 @@ public class SimpleCalculatedFieldStateTest {
"key3", key3ArgEntry
));
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
@ -151,7 +155,7 @@ public class SimpleCalculatedFieldStateTest {
"key3", key3ArgEntry
));
assertThatThrownBy(() -> state.performCalculation(ctx.getEntityId(), ctx))
assertThatThrownBy(() -> state.performCalculation(ctx))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Argument 'key2' is not a number.");
}
@ -164,7 +168,7 @@ public class SimpleCalculatedFieldStateTest {
"key3", key3ArgEntry
));
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
Output output = getCalculatedFieldConfig().getOutput();
@ -185,7 +189,7 @@ public class SimpleCalculatedFieldStateTest {
output.setDecimalsByDefault(3);
ctx.setOutput(output);
CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get();
TelemetryCalculatedFieldResult result = performCalculation();
assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType());
@ -265,4 +269,8 @@ public class SimpleCalculatedFieldStateTest {
return config;
}
private TelemetryCalculatedFieldResult performCalculation() throws InterruptedException, ExecutionException {
return (TelemetryCalculatedFieldResult) state.performCalculation(ctx).get();
}
}

7
application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java

@ -36,7 +36,6 @@ import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculat
import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -85,14 +84,14 @@ class CalculatedFieldUtilsTest {
geofencingArgumentEntry.setZoneStates(zoneStates);
// Create cf state with the geofencing argument and add it to the state map
CalculatedFieldState state = new GeofencingCalculatedFieldState(List.of("geofencingArgumentTest"));
state.updateState(mock(CalculatedFieldCtx.class), Map.of("geofencingArgumentTest", geofencingArgumentEntry));
CalculatedFieldState state = new GeofencingCalculatedFieldState(DEVICE_ID);
state.update(mock(CalculatedFieldCtx.class), Map.of("geofencingArgumentTest", geofencingArgumentEntry));
// when
CalculatedFieldStateProto proto = toProto(stateId, state);
// then
CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(proto);
CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(stateId, proto);
assertThat(fromProto)
.usingRecursiveComparison()
.ignoringFields("requiredArguments")

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

@ -74,7 +74,7 @@ public interface AlarmService extends EntityDaoService {
AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details);
AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent);
AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts);

2
common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java

@ -141,6 +141,8 @@ public enum MsgType {
CF_PARTITIONS_CHANGE_MSG, // Sent when cluster event occures;
CF_ENTITY_LIFECYCLE_MSG, // Sent on CF/Device/Asset create/update/delete;
CF_ENTITY_ACTION_EVENT_MSG,
CF_ALARM_ACTION_MSG,
CF_TELEMETRY_MSG, // Sent from queue to actor system;
CF_LINKED_TELEMETRY_MSG, // Sent from queue to actor system;

5
common/message/src/main/java/org/thingsboard/server/common/msg/ToCalculatedFieldSystemMsg.java

@ -16,12 +16,7 @@
package org.thingsboard.server.common.msg;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
public interface ToCalculatedFieldSystemMsg extends TenantAwareMsg {
default TbCallback getCallback() {
return TbCallback.EMPTY;
}
}

7
common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java

@ -17,9 +17,14 @@ package org.thingsboard.server.common.msg.aware;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.queue.TbCallback;
public interface TenantAwareMsg extends TbActorMsg {
TenantId getTenantId();
default TbCallback getCallback() {
return TbCallback.EMPTY;
}
}

23
common/proto/src/main/proto/queue.proto

@ -921,6 +921,7 @@ message CalculatedFieldStateProto {
repeated SingleValueArgumentProto singleValueArguments = 3;
repeated TsRollingArgumentProto rollingValueArguments = 4;
repeated GeofencingArgumentProto geofencingArguments = 5;
AlarmStateProto alarmState = 6;
}
//Used to report session state to tb-Service and persist this state in the cache on the tb-Service level.
@ -1721,10 +1722,13 @@ message ToEdgeEventNotificationMsg {
message ToCalculatedFieldMsg {
CalculatedFieldTelemetryMsgProto telemetryMsg = 1;
CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2;
EntityActionEventProto eventMsg = 3;
repeated string cfTypes = 4;
}
message ToCalculatedFieldNotificationMsg {
CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1;
repeated string cfTypes = 2;
}
/* Messages that are handled by ThingsBoard RuleEngine Service */
@ -1894,3 +1898,22 @@ message JobStatsMsg {
message TaskResultProto {
string value = 1;
}
message EntityActionEventProto {
EntityIdProto tenantId = 1;
EntityIdProto entityId = 2;
string entity = 3;
string action = 4;
}
message AlarmStateProto {
repeated AlarmRuleStateProto createRuleStates = 1;
AlarmRuleStateProto clearRuleState = 2;
}
message AlarmRuleStateProto {
string severity = 1;
int64 lastEventTs = 2;
int64 duration = 3;
int64 eventCount = 4;
}

14
common/util/src/main/java/org/thingsboard/common/util/ExpressionFunctionsUtil.java → common/util/src/main/java/org/thingsboard/common/util/ExpressionUtils.java

@ -15,13 +15,16 @@
*/
package org.thingsboard.common.util;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import net.objecthunter.exp4j.function.Function;
import net.objecthunter.exp4j.function.Functions;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class ExpressionFunctionsUtil {
public class ExpressionUtils {
public static final List<Function> userDefinedFunctions = new ArrayList<>();
@ -75,4 +78,13 @@ public class ExpressionFunctionsUtil {
userDefinedFunctions.add(Functions.getBuiltinFunction("signum"));
}
public static Expression createExpression(String expression, Set<String> variables) {
return new ExpressionBuilder(expression)
.functions(userDefinedFunctions)
.implicitMultiplication(true)
.operator()
.variables(variables)
.build();
}
}

31
common/util/src/main/java/org/thingsboard/common/util/KvUtil.java

@ -61,6 +61,37 @@ public class KvUtil {
}
}
public static Long getLongValue(KvEntry entry) {
switch (entry.getDataType()) {
case LONG -> {
return entry.getLongValue().orElse(null);
}
case DOUBLE -> {
return entry.getDoubleValue().map(Double::longValue).orElse(null);
}
case BOOLEAN -> {
return entry.getBooleanValue().map(b -> b ? 1L : 0L).orElse(null);
}
case STRING -> {
try {
return Long.parseLong(entry.getStrValue().orElse(""));
} catch (RuntimeException e) {
return null;
}
}
case JSON -> {
try {
return Long.parseLong(entry.getJsonValue().orElse(""));
} catch (RuntimeException e) {
return null;
}
}
default -> {
return null;
}
}
}
public static Boolean getBoolValue(KvEntry entry) {
switch (entry.getDataType()) {
case LONG:

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

@ -146,18 +146,26 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) {
var result = withPropagated(alarmDao.acknowledgeAlarm(tenantId, alarmId, ackTs));
if (result.getAlarm() != null) {
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(result.getAlarm().getId())
.actionType(ActionType.ALARM_ACK).build());
eventPublisher.publishEvent(ActionEntityEvent.builder()
.tenantId(tenantId)
.entityId(result.getAlarm().getId())
.entity(result.getAlarm())
.actionType(ActionType.ALARM_ACK)
.build());
}
return result;
}
@Override
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) {
public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent) {
var result = withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details));
if (result.getAlarm() != null) {
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entityId(result.getAlarm().getId())
.actionType(ActionType.ALARM_CLEAR).build());
eventPublisher.publishEvent(ActionEntityEvent.builder()
.tenantId(tenantId)
.entityId(result.getAlarm().getId())
.entity(result.getAlarm())
.actionType(ActionType.ALARM_CLEAR)
.build());
}
return result;
}
@ -205,6 +213,12 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
.entityId(alarm.getId())
.entity(alarm)
.build());
eventPublisher.publishEvent(ActionEntityEvent.builder()
.tenantId(tenantId)
.entityId(alarm.getId())
.entity(alarm)
.actionType(ActionType.ALARM_DELETE)
.build());
if (deleteAlarmTypes) {
delAlarmTypes(tenantId, Collections.singleton(alarm.getType()));
}

12
dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java

@ -211,7 +211,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true);
created = alarmService.findAlarmInfoById(tenantId, created.getId());
alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder()
@ -319,7 +319,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true);
created = alarmService.findAlarmInfoById(tenantId, created.getId());
alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
@ -622,7 +622,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.severity(AlarmSeverity.MAJOR)
.startTs(System.currentTimeMillis()).build());
AlarmInfo alarm1 = result.getAlarm();
alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null);
alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null, true);
result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(tenantId)
@ -632,7 +632,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.startTs(System.currentTimeMillis()).build());
AlarmInfo alarm2 = result.getAlarm();
alarmService.acknowledgeAlarm(tenantId, alarm2.getId(), System.currentTimeMillis());
alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null);
alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null, true);
result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(tenantId)
@ -852,7 +852,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
Assert.assertEquals(1, alarmsCount);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true);
created = alarmService.findAlarmInfoById(tenantId, created.getId());
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery);
@ -980,7 +980,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));
Assert.assertEquals(0, alarmsCount);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null);
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null, true);
countQuery.setStatusList(List.of(AlarmSearchStatus.CLEARED));
alarmsCount = alarmService.countAlarmsByQuery(tenantId, null, countQuery, List.of(childId));

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java

@ -63,6 +63,8 @@ public interface RuleEngineAlarmService {
AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details);
AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details, boolean pushEvent);
AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs);
AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs);

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java

@ -17,11 +17,13 @@ package org.thingsboard.rule.engine.action;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TbAlarmResult {
boolean isCreated;
boolean isUpdated;

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java

@ -22,8 +22,8 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.common.util.ExpressionUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.AttributesSaveRequest;
import org.thingsboard.rule.engine.api.RuleNode;
@ -53,7 +53,6 @@ import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions;
import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT;
@SuppressWarnings("UnstableApiUsage")
@ -310,11 +309,8 @@ public class TbMathNode implements TbNode {
case CUSTOM:
var expr = customExpression.get();
if (expr == null) {
expr = new ExpressionBuilder(config.getCustomFunction())
.functions(userDefinedFunctions)
.implicitMultiplication(true)
.variables(config.getArguments().stream().map(TbMathArgument::getName).collect(Collectors.toSet()))
.build();
expr = ExpressionUtils.createExpression(config.getCustomFunction(), config.getArguments().stream()
.map(TbMathArgument::getName).collect(Collectors.toSet()));
customExpression.set(expr);
}
for (int i = 0; i < config.getArguments().size(); i++) {

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

@ -115,16 +115,12 @@ class AlarmRuleState {
public AlarmEvalResult eval(DataSnapshot data) {
boolean active = isActive(data, data.getTs());
switch (spec.getType()) {
case SIMPLE:
return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE;
case DURATION:
return evalDuration(data, active);
case REPEATING:
return evalRepeating(data, active);
default:
return AlarmEvalResult.FALSE;
}
return switch (spec.getType()) {
case SIMPLE -> (active && eval(alarmRule.getCondition(), data)) ?
AlarmEvalResult.TRUE : AlarmEvalResult.FALSE;
case DURATION -> evalDuration(data, active);
case REPEATING -> evalRepeating(data, active);
};
}
private boolean isActive(DataSnapshot data, long eventTs) {
@ -600,4 +596,5 @@ class AlarmRuleState {
return null;
}
}
}

4
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java

@ -59,7 +59,6 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
@ -85,14 +84,12 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
@ -246,7 +243,6 @@ public class TbDeviceProfileNodeTest extends AbstractRuleNodeUpgradeTest {
node.onMsg(ctx, msg2);
verify(ctx).tellSuccess(msg2);
verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated");
}
@Test

Loading…
Cancel
Save