From 92ba4ff233376d2960079c81c51f563c09f7bb41 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 27 Oct 2025 15:58:51 +0200 Subject: [PATCH 01/14] added cf output strategy --- ...CalculatedFieldEntityMessageProcessor.java | 2 +- ...tractCalculatedFieldProcessingService.java | 77 +++++++++++++++++++ .../cf/AlarmCalculatedFieldResult.java | 7 ++ .../cf/CalculatedFieldProcessingService.java | 4 + .../service/cf/CalculatedFieldResult.java | 3 + ...faultCalculatedFieldProcessingService.java | 45 ++++++++--- .../cf/PropagationCalculatedFieldResult.java | 6 ++ .../cf/TelemetryCalculatedFieldResult.java | 2 + .../ctx/state/ScriptCalculatedFieldState.java | 1 + .../ctx/state/SimpleCalculatedFieldState.java | 1 + .../GeofencingCalculatedFieldState.java | 1 + .../PropagationCalculatedFieldState.java | 1 + .../cf/CalculatedFieldIntegrationTest.java | 45 +++++++++++ ...AttributeSkipRuleEngineOutputStrategy.java | 29 +++++++ .../common/data/cf/configuration/Output.java | 8 ++ .../data/cf/configuration/OutputStrategy.java | 37 +++++++++ .../cf/configuration/OutputStrategyType.java | 22 ++++++ .../PushToRuleEngineOutputStrategy.java | 25 ++++++ .../SkipRuleEngineOutputStrategy.java | 37 +++++++++ ...imeSeriesSkipRuleEngineOutputStrategy.java | 29 +++++++ 20 files changed, 372 insertions(+), 10 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PushToRuleEngineOutputStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SkipRuleEngineOutputStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 182d815c96..c3eeb71c7b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -406,7 +406,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM stateSizeChecked = true; if (state.isSizeOk()) { if (!calculationResult.isEmpty()) { - cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); + cfService.processResult(tenantId, entityId, calculationResult, cfIdList, callback); } else { callback.onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 5115dbc079..a570640f5d 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -15,18 +15,28 @@ */ package org.thingsboard.server.service.cf; +import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.common.adaptor.JsonConverter; 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.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesSkipRuleEngineOutputStrategy; +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.Aggregation; @@ -34,9 +44,11 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -44,10 +56,13 @@ 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.SingleValueArgumentEntry; +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -67,6 +82,7 @@ public abstract class AbstractCalculatedFieldProcessingService { protected final AttributesService attributesService; protected final TimeseriesService timeseriesService; + protected final TelemetrySubscriptionService tsSubService; protected final ApiLimitService apiLimitService; protected final RelationService relationService; protected final OwnerService ownerService; @@ -268,4 +284,65 @@ public abstract class AbstractCalculatedFieldProcessingService { return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE); } + protected void saveTelemetryResult(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, TbCallback callback) { + OutputType type = cfResult.getType(); + JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); + + log.trace("[{}][{}] Saving CF result: {}", tenantId, entityId, jsonResult); + + SettableFuture future = SettableFuture.create(); + switch (type) { + case ATTRIBUTES -> saveAttributes(tenantId, entityId, jsonResult, cfIds, future); + case TIME_SERIES -> saveTimeSeries(tenantId, entityId, jsonResult, ((TimeSeriesSkipRuleEngineOutputStrategy) cfResult.getOutputStrategy()).getTtl(), cfIds, System.currentTimeMillis(), TimeseriesSaveRequest.Strategy.PROCESS_ALL, future); + } + + if (log.isTraceEnabled()) { + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Void v) { + callback.onSuccess(); + log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, cfResult); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, cfResult, t); + } + }, MoreExecutors.directExecutor()); + } + } + + private void saveAttributes(TenantId tenantId, EntityId entityId, JsonElement jsonResult, List cfIds, SettableFuture future) { + List attributeKvEntries = JsonConverter.convertToAttributes(jsonResult); + tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .entries(attributeKvEntries) + .strategy(AttributesSaveRequest.Strategy.PROCESS_ALL) + .previousCalculatedFieldIds(cfIds) + .future(future) + .build() + ); + } + + private void saveTimeSeries(TenantId tenantId, EntityId entityId, JsonElement jsonResult, Long ttl, List cfIds, long ts, TimeseriesSaveRequest.Strategy strategy, SettableFuture future) { + Map> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts); + List tsEntries = new ArrayList<>(); + for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { + for (KvEntry kvEntry : tsKvEntry.getValue()) { + tsEntries.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); + } + } + tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .entries(tsEntries) + .ttl(ttl) + .strategy(strategy) + .previousCalculatedFieldIds(cfIds) + .future(future) + .build()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java index 498a215e17..3191b84193 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java @@ -21,6 +21,8 @@ import lombok.RequiredArgsConstructor; 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.cf.configuration.OutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.PushToRuleEngineOutputStrategy; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -36,6 +38,11 @@ public class AlarmCalculatedFieldResult implements CalculatedFieldResult { private final TbAlarmResult alarmResult; + @Override + public OutputStrategy getOutputStrategy() { + return new PushToRuleEngineOutputStrategy(); + } + @Override public TbMsg toTbMsg(EntityId entityId, List cfIds) { TbMsgType msgType; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index a9139572b8..5473f3f4a9 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -37,6 +37,10 @@ public interface CalculatedFieldProcessingService { Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); + void saveToDB(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); + + void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); + void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List linkedCalculatedFields, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index c62d5dc6d5..a9c2c532ee 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.cf; +import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; @@ -23,6 +24,8 @@ import java.util.List; public interface CalculatedFieldResult { + OutputStrategy getOutputStrategy(); + TbMsg toTbMsg(EntityId entityId, List cfIds); String stringValue(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 52393d0ffe..3cec856746 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.cf; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.TriConsumer; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; @@ -47,6 +48,7 @@ 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.telemetry.TelemetrySubscriptionService; import java.util.ArrayList; import java.util.Collections; @@ -72,8 +74,9 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF RelationService relationService, OwnerService ownerService, TbClusterService clusterService, + TelemetrySubscriptionService tsSubService, PartitionService partitionService) { - super(attributesService, timeseriesService, apiLimitService, relationService, ownerService); + super(attributesService, timeseriesService, tsSubService, apiLimitService, relationService, ownerService); this.clusterService = clusterService; this.partitionService = partitionService; } @@ -111,27 +114,51 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF return resolveArgumentFutures(argFutures); } + @Override + public void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { + switch (result.getOutputStrategy().getType()) { + case SKIP_RULE_ENGINE -> saveToDB(tenantId, entityId, result, cfIds, callback); + case PUSH_TO_RULE_ENGINE -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); + } + } + + @Override + public void saveToDB(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { + if (result instanceof TelemetryCalculatedFieldResult telemetryResult) { + saveTelemetryResult(tenantId, entityId, telemetryResult, cfIds, callback); + return; + } + if (result instanceof PropagationCalculatedFieldResult propagationResult) { + handlePropagationResults(propagationResult, callback, + (entity, res, cb) -> saveTelemetryResult(tenantId, entityId, res, cfIds, cb)); + } + } + @Override public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { - if (!(result instanceof PropagationCalculatedFieldResult propagationCalculatedFieldResult)) { - TbMsg msg = result.toTbMsg(entityId, cfIds); - sendMsgToRuleEngine(tenantId, entityId, callback, msg); + if (result instanceof PropagationCalculatedFieldResult propagationResult) { + handlePropagationResults(propagationResult, callback, + (entity, res, cb) -> sendMsgToRuleEngine(tenantId, entityId, cb, res.toTbMsg(entity, cfIds))); return; } - List propagationEntityIds = propagationCalculatedFieldResult.getPropagationEntityIds(); + + sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfIds)); + } + + private void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback, + TriConsumer telemetryResultHandler) { + List propagationEntityIds = propagationResult.getPropagationEntityIds(); if (propagationEntityIds.isEmpty()) { callback.onSuccess(); } if (propagationEntityIds.size() == 1) { EntityId propagationEntityId = propagationEntityIds.get(0); - TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); - sendMsgToRuleEngine(tenantId, propagationEntityId, callback, msg); + telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), callback); return; } MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback); for (var propagationEntityId : propagationEntityIds) { - TbMsg msg = result.toTbMsg(propagationEntityId, cfIds); - sendMsgToRuleEngine(tenantId, propagationEntityId, multipleTbCallback, msg); + telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), multipleTbCallback); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java index 780fd220a7..38e1464fb3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.cf; import lombok.Builder; import lombok.Data; +import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.util.CollectionsUtil; @@ -31,6 +32,11 @@ public final class PropagationCalculatedFieldResult implements CalculatedFieldRe private final List propagationEntityIds; private final TelemetryCalculatedFieldResult result; + @Override + public OutputStrategy getOutputStrategy() { + return result.getOutputStrategy(); + } + @Override public TbMsg toTbMsg(EntityId entityId, List cfIds) { return result.toTbMsg(entityId, cfIds); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java index 1ad666eac5..69c996c3cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/TelemetryCalculatedFieldResult.java @@ -19,6 +19,7 @@ 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.OutputStrategy; 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; @@ -37,6 +38,7 @@ public final class TelemetryCalculatedFieldResult implements CalculatedFieldResu private final OutputType type; private final AttributeScope scope; + private final OutputStrategy outputStrategy; private final JsonNode result; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index c52c01549f..7a395284b3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -52,6 +52,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { Output output = ctx.getOutput(); return Futures.transform(resultFuture, result -> TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) .type(output.getType()) .scope(output.getScope()) .result(JacksonUtil.valueToTree(result)) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index c3d8c3e63b..462c97aa02 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -56,6 +56,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result); return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) .type(output.getType()) .scope(output.getScope()) .result(outputResult) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index 51110df2bb..b3ea94e62c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -130,6 +130,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { OutputType outputType = ctx.getOutput().getType(); var result = TelemetryCalculatedFieldResult.builder() + .outputStrategy(ctx.getOutput().getStrategy()) .type(outputType) .scope(ctx.getOutput().getScope()) .result(toResultNode(outputType, valuesNode)) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index 01e9a73de8..fbb8d64581 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -91,6 +91,7 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState Output output = ctx.getOutput(); TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) .type(output.getType()) .scope(output.getScope()); ObjectNode valuesNode = JacksonUtil.newObjectNode(); diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 209a2da6f1..b3f75a1919 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -35,12 +35,15 @@ 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.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.SkipRuleEngineOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesSkipRuleEngineOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -1162,6 +1165,48 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testSimpleCalculatedFieldWhenSkipRuleEngineOutputProcessing() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + + postTelemetry(testDevice.getId(), "{\"temperature\":24.5}"); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("C to F"); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + config.setArguments(Map.of("T", argument)); + config.setExpression("(T * 9/5) + 32"); + + Output output = new Output(); + output.setName("fahrenheitTemp"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(1); + output.setStrategy(new TimeSeriesSkipRuleEngineOutputStrategy(1000L)); + + config.setOutput(output); + + config.setUseLatestTs(true); + + calculatedField.setConfiguration(config); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode fahrenheitTemp = getLatestTelemetry(testDevice.getId(), "fahrenheitTemp"); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("76.1"); + }); + } private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java new file mode 100644 index 0000000000..b796042de3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java @@ -0,0 +1,29 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AttributeSkipRuleEngineOutputStrategy extends SkipRuleEngineOutputStrategy { + + private boolean updateAttributesOnlyOnValueChange; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index f2b4948837..1821db2760 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import org.thingsboard.server.common.data.AttributeScope; @@ -28,4 +29,11 @@ public class Output { private AttributeScope scope; private Integer decimalsByDefault; + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + property = "type" + ) + private OutputStrategy strategy; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java new file mode 100644 index 0000000000..b4b71103e8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -0,0 +1,37 @@ +/** + * 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.common.data.cf.configuration; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SkipRuleEngineOutputStrategy.class, name = "SKIP_RULE_ENGINE"), + @JsonSubTypes.Type(value = PushToRuleEngineOutputStrategy.class, name = "PUSH_TO_RULE_ENGINE") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface OutputStrategy { + + OutputStrategyType getType(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java new file mode 100644 index 0000000000..d4eef18d61 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java @@ -0,0 +1,22 @@ +/** + * 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.common.data.cf.configuration; + +public enum OutputStrategyType { + + SKIP_RULE_ENGINE, PUSH_TO_RULE_ENGINE + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PushToRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PushToRuleEngineOutputStrategy.java new file mode 100644 index 0000000000..adeab6c35d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PushToRuleEngineOutputStrategy.java @@ -0,0 +1,25 @@ +/** + * 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.common.data.cf.configuration; + +public class PushToRuleEngineOutputStrategy implements OutputStrategy { + + @Override + public OutputStrategyType getType() { + return OutputStrategyType.PUSH_TO_RULE_ENGINE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SkipRuleEngineOutputStrategy.java new file mode 100644 index 0000000000..dfc2873ccb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SkipRuleEngineOutputStrategy.java @@ -0,0 +1,37 @@ +/** + * 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.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXTERNAL_PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AttributeSkipRuleEngineOutputStrategy.class, name = "ATTRIBUTES"), + @JsonSubTypes.Type(value = TimeSeriesSkipRuleEngineOutputStrategy.class, name = "TIME_SERIES") +}) +public abstract class SkipRuleEngineOutputStrategy implements OutputStrategy { + + @Override + public OutputStrategyType getType() { + return OutputStrategyType.SKIP_RULE_ENGINE; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java new file mode 100644 index 0000000000..27cc561035 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java @@ -0,0 +1,29 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TimeSeriesSkipRuleEngineOutputStrategy extends SkipRuleEngineOutputStrategy { + + private long ttl; + +} From 76606f63ba1036b5577dd9acdd60a12c8b0b5a23 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 3 Nov 2025 12:14:44 +0200 Subject: [PATCH 02/14] added upgrade script --- .../main/data/upgrade/basic/schema_update.sql | 19 +++++ ...tractCalculatedFieldProcessingService.java | 85 ++++++++++++++++--- ...faultCalculatedFieldProcessingService.java | 2 +- .../cf/CalculatedFieldIntegrationTest.java | 2 +- ...AttributeSkipRuleEngineOutputStrategy.java | 4 + ...imeSeriesSkipRuleEngineOutputStrategy.java | 5 ++ 6 files changed, 101 insertions(+), 16 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index fe79fce3a2..cc4fbaad14 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -69,3 +69,22 @@ ALTER TABLE calculated_field DROP CONSTRAINT IF EXISTS calculated_field_unq_key; ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, type, name); -- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END + +-- CALCULATED FIELD OUTPUT STRATEGY UPGRADE START + +UPDATE calculated_field +SET configuration = jsonb_set( + configuration::jsonb, + '{output}', + (configuration::jsonb -> 'output') + || jsonb_build_object( + 'strategy', + jsonb_build_object( + 'type', 'PUSH_TO_RULE_ENGINE' + ) + ), + false + ) +WHERE (configuration::jsonb -> 'output' -> 'strategy') IS NULL; + +-- CALCULATED FIELD OUTPUT STRATEGY UPGRADE END diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 455093a116..f9c5d6db78 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -27,12 +27,15 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rule.engine.api.AttributesSaveRequest; +import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.adaptor.JsonConverter; 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.AttributeSkipRuleEngineOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.TimeSeriesSkipRuleEngineOutputStrategy; @@ -71,6 +74,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.function.Function; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION; @@ -344,8 +348,8 @@ public abstract class AbstractCalculatedFieldProcessingService { SettableFuture future = SettableFuture.create(); switch (type) { - case ATTRIBUTES -> saveAttributes(tenantId, entityId, jsonResult, cfIds, future); - case TIME_SERIES -> saveTimeSeries(tenantId, entityId, jsonResult, ((TimeSeriesSkipRuleEngineOutputStrategy) cfResult.getOutputStrategy()).getTtl(), cfIds, System.currentTimeMillis(), TimeseriesSaveRequest.Strategy.PROCESS_ALL, future); + case ATTRIBUTES -> saveAttributes(tenantId, entityId, cfResult, cfIds, future); + case TIME_SERIES -> saveTimeSeries(tenantId, entityId, cfResult, cfIds, System.currentTimeMillis(), future); } if (log.isTraceEnabled()) { @@ -365,20 +369,54 @@ public abstract class AbstractCalculatedFieldProcessingService { } } - private void saveAttributes(TenantId tenantId, EntityId entityId, JsonElement jsonResult, List cfIds, SettableFuture future) { + private void saveAttributes(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, SettableFuture future) { + if (!(cfResult.getOutputStrategy() instanceof AttributeSkipRuleEngineOutputStrategy outputStrategy)) { + return; + } + JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); + + AttributesSaveRequest.Strategy strategy = new Strategy(outputStrategy.isSaveAttribute(), outputStrategy.isSendWsUpdate(), outputStrategy.isProcessCfs()); List attributeKvEntries = JsonConverter.convertToAttributes(jsonResult); - tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() - .tenantId(tenantId) - .entityId(entityId) - .entries(attributeKvEntries) - .strategy(AttributesSaveRequest.Strategy.PROCESS_ALL) - .previousCalculatedFieldIds(cfIds) - .future(future) - .build() - ); + + if (!outputStrategy.isUpdateAttributesOnlyOnValueChange()) { + tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .entries(attributeKvEntries) + .strategy(strategy) + .previousCalculatedFieldIds(cfIds) + .future(future) + .build() + ); + return; + } + + List keys = attributeKvEntries.stream().map(KvEntry::getKey).collect(Collectors.toList()); + + ListenableFuture> findFuture = attributesService.find(tenantId, entityId, cfResult.getScope(), keys); + + DonAsynchron.withCallback(findFuture, + existingAttributes -> { + List attributesChanged = filterChangedAttr(existingAttributes, attributeKvEntries); + tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .entries(attributesChanged) + .strategy(strategy) + .previousCalculatedFieldIds(cfIds) + .future(future) + .build() + ); + }, + future::setException, + MoreExecutors.directExecutor()); } - private void saveTimeSeries(TenantId tenantId, EntityId entityId, JsonElement jsonResult, Long ttl, List cfIds, long ts, TimeseriesSaveRequest.Strategy strategy, SettableFuture future) { + private void saveTimeSeries(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, long ts, SettableFuture future) { + if (!(cfResult.getOutputStrategy() instanceof TimeSeriesSkipRuleEngineOutputStrategy outputStrategy)) { + return; + } + JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); Map> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts); List tsEntries = new ArrayList<>(); for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { @@ -386,15 +424,34 @@ public abstract class AbstractCalculatedFieldProcessingService { tsEntries.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); } } + TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(outputStrategy.isSaveTimeSeries(), outputStrategy.isSaveLatest(), outputStrategy.isSendWsUpdate(), outputStrategy.isProcessCfs()); tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() .tenantId(tenantId) .entityId(entityId) .entries(tsEntries) - .ttl(ttl) + .ttl(outputStrategy.getTtl()) .strategy(strategy) .previousCalculatedFieldIds(cfIds) .future(future) .build()); } + private List filterChangedAttr(List existingAttributes, List newAttributes) { + if (existingAttributes == null || existingAttributes.isEmpty()) { + return newAttributes; + } + + Map currentAttrMap = existingAttributes.stream() + .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); + + return newAttributes.stream() + .filter(item -> { + AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); + return cacheAttr == null + || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type + || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); + }) + .collect(Collectors.toList()); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 3cec856746..a6058c7dcb 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -17,7 +17,7 @@ package org.thingsboard.server.service.cf; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.logging.log4j.util.TriConsumer; +import org.apache.commons.lang3.function.TriConsumer; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 874d5c8cd6..6a76a2fb99 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -1235,7 +1235,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes output.setName("fahrenheitTemp"); output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(1); - output.setStrategy(new TimeSeriesSkipRuleEngineOutputStrategy(1000L)); + output.setStrategy(new TimeSeriesSkipRuleEngineOutputStrategy(1000L, true, true, true, true)); config.setOutput(output); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java index b796042de3..5d30eb36f9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java @@ -26,4 +26,8 @@ public class AttributeSkipRuleEngineOutputStrategy extends SkipRuleEngineOutputS private boolean updateAttributesOnlyOnValueChange; + private boolean saveAttribute; + private boolean sendWsUpdate; + private boolean processCfs; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java index 27cc561035..3165fc5eac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java @@ -26,4 +26,9 @@ public class TimeSeriesSkipRuleEngineOutputStrategy extends SkipRuleEngineOutput private long ttl; + private boolean saveTimeSeries; + private boolean saveLatest; + private boolean sendWsUpdate; + private boolean processCfs; + } From 31ed28a6feaffd9d94a70a429f8ce4326c1c3dfd Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 11 Nov 2025 09:46:48 +0200 Subject: [PATCH 03/14] restructured output strategies --- .../main/data/upgrade/basic/schema_update.sql | 6 +- ...tractCalculatedFieldProcessingService.java | 84 ++++++++----------- .../cf/AlarmCalculatedFieldResult.java | 7 -- .../cf/CalculatedFieldProcessingService.java | 2 +- .../service/cf/CalculatedFieldResult.java | 3 - ...faultCalculatedFieldProcessingService.java | 17 +++- .../cf/PropagationCalculatedFieldResult.java | 6 -- .../cf/CalculatedFieldIntegrationTest.java | 4 +- ... => AttributeImmediateOutputStrategy.java} | 8 +- ... => AttributeRuleChainOutputStrategy.java} | 22 ++--- ...tegy.java => ImmediateOutputStrategy.java} | 6 +- .../common/data/cf/configuration/Output.java | 7 -- .../data/cf/configuration/OutputStrategy.java | 22 +++-- .../cf/configuration/OutputStrategyType.java | 2 +- .../RuleChainOutputStrategy.java | 25 ++++++ ...=> TimeSeriesImmediateOutputStrategy.java} | 8 +- .../TimeSeriesRuleChainOutputStrategy.java | 29 +++++++ .../engine/telemetry/TbMsgAttributesNode.java | 22 +---- .../engine/telemetry/TbMsgTimeseriesNode.java | 8 +- .../rule/engine/util/TelemetryUtil.java | 60 +++++++++++++ 20 files changed, 209 insertions(+), 139 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{AttributeSkipRuleEngineOutputStrategy.java => AttributeImmediateOutputStrategy.java} (85%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{SkipRuleEngineOutputStrategy.java => AttributeRuleChainOutputStrategy.java} (52%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{PushToRuleEngineOutputStrategy.java => ImmediateOutputStrategy.java} (80%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{TimeSeriesSkipRuleEngineOutputStrategy.java => TimeSeriesImmediateOutputStrategy.java} (85%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index cc4fbaad14..565ac50afd 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -80,7 +80,11 @@ SET configuration = jsonb_set( || jsonb_build_object( 'strategy', jsonb_build_object( - 'type', 'PUSH_TO_RULE_ENGINE' + 'type', + CASE (configuration::jsonb -> 'output' ->> 'type') + WHEN 'TIME_SERIES' THEN 'RULE_CHAIN_TIME_SERIES' + WHEN 'ATTRIBUTES' THEN 'RULE_CHAIN_ATTRIBUTES' + END ) ), false diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index ba3f2d4cc5..5ecbeeb496 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -36,10 +36,10 @@ import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.cf.CalculatedField; 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.AttributeSkipRuleEngineOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.AttributeImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; -import org.thingsboard.server.common.data.cf.configuration.TimeSeriesSkipRuleEngineOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -66,7 +66,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -75,10 +74,11 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import static org.thingsboard.rule.engine.util.TelemetryUtil.filterChangedAttr; +import static org.thingsboard.rule.engine.util.TelemetryUtil.toTsKvEntryList; import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; @@ -378,60 +378,60 @@ public abstract class AbstractCalculatedFieldProcessingService { } private void saveAttributes(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, SettableFuture future) { - if (!(cfResult.getOutputStrategy() instanceof AttributeSkipRuleEngineOutputStrategy outputStrategy)) { + if (!(cfResult.getOutputStrategy() instanceof AttributeImmediateOutputStrategy outputStrategy)) { + future.setException(new IllegalArgumentException("Expected AttributeImmediateOutputStrategy")); return; } JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); AttributesSaveRequest.Strategy strategy = new Strategy(outputStrategy.isSaveAttribute(), outputStrategy.isSendWsUpdate(), outputStrategy.isProcessCfs()); - List attributeKvEntries = JsonConverter.convertToAttributes(jsonResult); + List newAttributes = JsonConverter.convertToAttributes(jsonResult); if (!outputStrategy.isUpdateAttributesOnlyOnValueChange()) { - tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() - .tenantId(tenantId) - .entityId(entityId) - .entries(attributeKvEntries) - .strategy(strategy) - .previousCalculatedFieldIds(cfIds) - .future(future) - .build() - ); + saveAttributesInternal(tenantId, entityId, cfResult, cfIds, newAttributes, strategy, future); return; } - List keys = attributeKvEntries.stream().map(KvEntry::getKey).collect(Collectors.toList()); - + List keys = newAttributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); ListenableFuture> findFuture = attributesService.find(tenantId, entityId, cfResult.getScope(), keys); DonAsynchron.withCallback(findFuture, existingAttributes -> { - List attributesChanged = filterChangedAttr(existingAttributes, attributeKvEntries); - tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() - .tenantId(tenantId) - .entityId(entityId) - .entries(attributesChanged) - .strategy(strategy) - .previousCalculatedFieldIds(cfIds) - .future(future) - .build() - ); + List changed = filterChangedAttr(existingAttributes, newAttributes); + saveAttributesInternal(tenantId, entityId, cfResult, cfIds, changed, strategy, future); }, future::setException, MoreExecutors.directExecutor()); } + private void saveAttributesInternal(TenantId tenantId, EntityId entityId, + TelemetryCalculatedFieldResult cfResult, + List cfIds, + List entries, + AttributesSaveRequest.Strategy strategy, + SettableFuture future) { + tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() + .tenantId(tenantId) + .entityId(entityId) + .scope(cfResult.getScope()) + .entries(entries) + .strategy(strategy) + .previousCalculatedFieldIds(cfIds) + .future(future) + .build()); + } + private void saveTimeSeries(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, long ts, SettableFuture future) { - if (!(cfResult.getOutputStrategy() instanceof TimeSeriesSkipRuleEngineOutputStrategy outputStrategy)) { + if (!(cfResult.getOutputStrategy() instanceof TimeSeriesImmediateOutputStrategy outputStrategy)) { + future.setException(new IllegalArgumentException("Expected TimeSeriesImmediateOutputStrategy")); return; } JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); Map> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts); - List tsEntries = new ArrayList<>(); - for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { - for (KvEntry kvEntry : tsKvEntry.getValue()) { - tsEntries.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); - } + if (tsKvMap.isEmpty()) { + future.setFuture(Futures.immediateFuture(null)); } + List tsEntries = toTsKvEntryList(tsKvMap); TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(outputStrategy.isSaveTimeSeries(), outputStrategy.isSaveLatest(), outputStrategy.isSendWsUpdate(), outputStrategy.isProcessCfs()); tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() .tenantId(tenantId) @@ -444,22 +444,4 @@ public abstract class AbstractCalculatedFieldProcessingService { .build()); } - private List filterChangedAttr(List existingAttributes, List newAttributes) { - if (existingAttributes == null || existingAttributes.isEmpty()) { - return newAttributes; - } - - Map currentAttrMap = existingAttributes.stream() - .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); - - return newAttributes.stream() - .filter(item -> { - AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); - return cacheAttr == null - || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type - || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); - }) - .collect(Collectors.toList()); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java index 3191b84193..498a215e17 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AlarmCalculatedFieldResult.java @@ -21,8 +21,6 @@ import lombok.RequiredArgsConstructor; 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.cf.configuration.OutputStrategy; -import org.thingsboard.server.common.data.cf.configuration.PushToRuleEngineOutputStrategy; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.msg.TbMsgType; @@ -38,11 +36,6 @@ public class AlarmCalculatedFieldResult implements CalculatedFieldResult { private final TbAlarmResult alarmResult; - @Override - public OutputStrategy getOutputStrategy() { - return new PushToRuleEngineOutputStrategy(); - } - @Override public TbMsg toTbMsg(EntityId entityId, List cfIds) { TbMsgType msgType; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index 5473f3f4a9..0475cb1b66 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -37,7 +37,7 @@ public interface CalculatedFieldProcessingService { Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); - void saveToDB(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); + void processImmediately(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index a9c2c532ee..c62d5dc6d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.cf; -import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; @@ -24,8 +23,6 @@ import java.util.List; public interface CalculatedFieldResult { - OutputStrategy getOutputStrategy(); - TbMsg toTbMsg(EntityId entityId, List cfIds); String stringValue(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index a6058c7dcb..09fdf62ddf 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -116,14 +116,20 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF @Override public void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { - switch (result.getOutputStrategy().getType()) { - case SKIP_RULE_ENGINE -> saveToDB(tenantId, entityId, result, cfIds, callback); - case PUSH_TO_RULE_ENGINE -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); + if (result instanceof AlarmCalculatedFieldResult) { + sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfIds)); + return; + } + TelemetryCalculatedFieldResult telemetryResult = result instanceof TelemetryCalculatedFieldResult telemetryRes + ? telemetryRes : ((PropagationCalculatedFieldResult) result).getResult(); + switch (telemetryResult.getOutputStrategy().getStrategyType()) { + case IMMEDIATE -> processImmediately(tenantId, entityId, result, cfIds, callback); + case RULE_CHAIN -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); } } @Override - public void saveToDB(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { + public void processImmediately(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { if (result instanceof TelemetryCalculatedFieldResult telemetryResult) { saveTelemetryResult(tenantId, entityId, telemetryResult, cfIds, callback); return; @@ -131,7 +137,9 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF if (result instanceof PropagationCalculatedFieldResult propagationResult) { handlePropagationResults(propagationResult, callback, (entity, res, cb) -> saveTelemetryResult(tenantId, entityId, res, cfIds, cb)); + return; } + callback.onSuccess(); } @Override @@ -150,6 +158,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF List propagationEntityIds = propagationResult.getPropagationEntityIds(); if (propagationEntityIds.isEmpty()) { callback.onSuccess(); + return; } if (propagationEntityIds.size() == 1) { EntityId propagationEntityId = propagationEntityIds.get(0); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java index 38e1464fb3..780fd220a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/PropagationCalculatedFieldResult.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.cf; import lombok.Builder; import lombok.Data; -import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.util.CollectionsUtil; @@ -32,11 +31,6 @@ public final class PropagationCalculatedFieldResult implements CalculatedFieldRe private final List propagationEntityIds; private final TelemetryCalculatedFieldResult result; - @Override - public OutputStrategy getOutputStrategy() { - return result.getOutputStrategy(); - } - @Override public TbMsg toTbMsg(EntityId entityId, List cfIds) { return result.toTbMsg(entityId, cfIds); diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 6a76a2fb99..8593ea8ae5 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -41,7 +41,7 @@ import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.TimeSeriesSkipRuleEngineOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -1235,7 +1235,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes output.setName("fahrenheitTemp"); output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(1); - output.setStrategy(new TimeSeriesSkipRuleEngineOutputStrategy(1000L, true, true, true, true)); + output.setStrategy(new TimeSeriesImmediateOutputStrategy(1000L, true, true, true, true)); config.setOutput(output); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java similarity index 85% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java index 5d30eb36f9..5ca28d0ee4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeSkipRuleEngineOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java @@ -20,9 +20,9 @@ import lombok.Data; import lombok.NoArgsConstructor; @Data -@NoArgsConstructor @AllArgsConstructor -public class AttributeSkipRuleEngineOutputStrategy extends SkipRuleEngineOutputStrategy { +@NoArgsConstructor +public class AttributeImmediateOutputStrategy extends ImmediateOutputStrategy { private boolean updateAttributesOnlyOnValueChange; @@ -30,4 +30,8 @@ public class AttributeSkipRuleEngineOutputStrategy extends SkipRuleEngineOutputS private boolean sendWsUpdate; private boolean processCfs; + @Override + public String getType() { + return "IMMEDIATE_ATTRIBUTES"; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java similarity index 52% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SkipRuleEngineOutputStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java index dfc2873ccb..adefc06964 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SkipRuleEngineOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java @@ -15,23 +15,15 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; +import lombok.NoArgsConstructor; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXTERNAL_PROPERTY, - property = "type" -) -@JsonSubTypes({ - @JsonSubTypes.Type(value = AttributeSkipRuleEngineOutputStrategy.class, name = "ATTRIBUTES"), - @JsonSubTypes.Type(value = TimeSeriesSkipRuleEngineOutputStrategy.class, name = "TIME_SERIES") -}) -public abstract class SkipRuleEngineOutputStrategy implements OutputStrategy { +@Data +@NoArgsConstructor +public class AttributeRuleChainOutputStrategy extends RuleChainOutputStrategy { @Override - public OutputStrategyType getType() { - return OutputStrategyType.SKIP_RULE_ENGINE; + public String getType() { + return "RULE_CHAIN_ATTRIBUTES"; } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PushToRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ImmediateOutputStrategy.java similarity index 80% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PushToRuleEngineOutputStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ImmediateOutputStrategy.java index adeab6c35d..170fa5bb9c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/PushToRuleEngineOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ImmediateOutputStrategy.java @@ -15,11 +15,11 @@ */ package org.thingsboard.server.common.data.cf.configuration; -public class PushToRuleEngineOutputStrategy implements OutputStrategy { +public abstract class ImmediateOutputStrategy implements OutputStrategy { @Override - public OutputStrategyType getType() { - return OutputStrategyType.PUSH_TO_RULE_ENGINE; + public OutputStrategyType getStrategyType() { + return OutputStrategyType.IMMEDIATE; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index 1821db2760..d2e0ac1ca9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Data; import org.thingsboard.server.common.data.AttributeScope; @@ -28,12 +27,6 @@ public class Output { private OutputType type; private AttributeScope scope; private Integer decimalsByDefault; - - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.EXTERNAL_PROPERTY, - property = "type" - ) private OutputStrategy strategy; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java index b4b71103e8..8f488602fe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -15,23 +15,31 @@ */ package org.thingsboard.server.common.data.cf.configuration; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +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.fasterxml.jackson.annotation.JsonTypeInfo.As; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, + include = As.PROPERTY, property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = SkipRuleEngineOutputStrategy.class, name = "SKIP_RULE_ENGINE"), - @JsonSubTypes.Type(value = PushToRuleEngineOutputStrategy.class, name = "PUSH_TO_RULE_ENGINE") + @Type(value = AttributeImmediateOutputStrategy.class, name = "IMMEDIATE_ATTRIBUTES"), + @Type(value = TimeSeriesImmediateOutputStrategy.class, name = "IMMEDIATE_TIME_SERIES"), + + @Type(value = AttributeRuleChainOutputStrategy.class, name = "RULE_CHAIN_ATTRIBUTES"), + @Type(value = TimeSeriesRuleChainOutputStrategy.class, name = "RULE_CHAIN_TIME_SERIES") + }) -@JsonIgnoreProperties(ignoreUnknown = true) public interface OutputStrategy { - OutputStrategyType getType(); + @JsonIgnore + OutputStrategyType getStrategyType(); + + @JsonIgnore + String getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java index d4eef18d61..4f5234acb5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java @@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration; public enum OutputStrategyType { - SKIP_RULE_ENGINE, PUSH_TO_RULE_ENGINE + IMMEDIATE, RULE_CHAIN } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java new file mode 100644 index 0000000000..0d601d22fc --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java @@ -0,0 +1,25 @@ +/** + * 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.common.data.cf.configuration; + +public abstract class RuleChainOutputStrategy implements OutputStrategy { + + @Override + public OutputStrategyType getStrategyType() { + return OutputStrategyType.RULE_CHAIN; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java similarity index 85% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java index 3165fc5eac..60d5f99515 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesSkipRuleEngineOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java @@ -20,9 +20,9 @@ import lombok.Data; import lombok.NoArgsConstructor; @Data -@NoArgsConstructor @AllArgsConstructor -public class TimeSeriesSkipRuleEngineOutputStrategy extends SkipRuleEngineOutputStrategy { +@NoArgsConstructor +public class TimeSeriesImmediateOutputStrategy extends ImmediateOutputStrategy { private long ttl; @@ -31,4 +31,8 @@ public class TimeSeriesSkipRuleEngineOutputStrategy extends SkipRuleEngineOutput private boolean sendWsUpdate; private boolean processCfs; + @Override + public String getType() { + return "IMMEDIATE_TIME_SERIES"; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java new file mode 100644 index 0000000000..bb50cc5e8b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java @@ -0,0 +1,29 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class TimeSeriesRuleChainOutputStrategy extends RuleChainOutputStrategy { + + @Override + public String getType() { + return "RULE_CHAIN_TIME_SERIES"; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 533f7d13dd..20aa7993a1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -41,16 +41,14 @@ import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.UUID; -import java.util.function.Function; import java.util.stream.Collectors; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.Advanced; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.Deduplicate; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.OnEveryMessage; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.WebSocketsOnly; +import static org.thingsboard.rule.engine.util.TelemetryUtil.filterChangedAttr; import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; @@ -216,24 +214,6 @@ public class TbMsgAttributesNode implements TbNode { .build()); } - private List filterChangedAttr(List currentAttributes, List newAttributes) { - if (currentAttributes == null || currentAttributes.isEmpty()) { - return newAttributes; - } - - Map currentAttrMap = currentAttributes.stream() - .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); - - return newAttributes.stream() - .filter(item -> { - AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); - return cacheAttr == null - || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type - || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); - }) - .collect(Collectors.toList()); - } - private boolean checkSendNotification(AttributeScope scope) { return config.isSendAttributesUpdatedNotification() && AttributeScope.CLIENT_SCOPE != scope; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 13dab98c54..32f06b1e00 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -49,6 +49,7 @@ import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessin import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.Deduplicate; import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.OnEveryMessage; import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.WebSocketsOnly; +import static org.thingsboard.rule.engine.util.TelemetryUtil.toTsKvEntryList; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @RuleNode( @@ -148,12 +149,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Msg body is empty: " + src)); return; } - List tsKvEntryList = new ArrayList<>(); - for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { - for (KvEntry kvEntry : tsKvEntry.getValue()) { - tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); - } - } + List tsKvEntryList = toTsKvEntryList(tsKvMap); String ttlValue = msg.getMetaData().getValue("TTL"); long ttl = !StringUtils.isEmpty(ttlValue) ? Long.parseLong(ttlValue) : config.getDefaultTTL(); if (ttl == 0L) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java new file mode 100644 index 0000000000..41d6f1ce1d --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java @@ -0,0 +1,60 @@ +/** + * 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.rule.engine.util; + +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class TelemetryUtil { + + public static List toTsKvEntryList(Map> tsKvMap) { + List tsKvEntryList = new ArrayList<>(); + for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { + for (KvEntry kvEntry : tsKvEntry.getValue()) { + tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); + } + } + return tsKvEntryList; + } + + public static List filterChangedAttr(List currentAttributes, List newAttributes) { + if (currentAttributes == null || currentAttributes.isEmpty()) { + return newAttributes; + } + + Map currentAttrMap = currentAttributes.stream() + .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); + + return newAttributes.stream() + .filter(item -> { + AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); + return cacheAttr == null + || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type + || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); + }) + .collect(Collectors.toList()); + } + +} From 256f4c3121a2d4f4acdded5cec7c74e217ca2436 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 11 Nov 2025 10:53:49 +0200 Subject: [PATCH 04/14] added checks when no updated attributes --- application/src/main/data/upgrade/basic/schema_update.sql | 4 ++-- .../cf/AbstractCalculatedFieldProcessingService.java | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 565ac50afd..a7909a358a 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -70,7 +70,7 @@ ALTER TABLE calculated_field ADD CONSTRAINT calculated_field_unq_key UNIQUE (ent -- CALCULATED FIELD UNIQUE CONSTRAINT UPDATE END --- CALCULATED FIELD OUTPUT STRATEGY UPGRADE START +-- CALCULATED FIELD OUTPUT STRATEGY UPDATE START UPDATE calculated_field SET configuration = jsonb_set( @@ -91,4 +91,4 @@ SET configuration = jsonb_set( ) WHERE (configuration::jsonb -> 'output' -> 'strategy') IS NULL; --- CALCULATED FIELD OUTPUT STRATEGY UPGRADE END +-- CALCULATED FIELD OUTPUT STRATEGY UPDATE END diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 5ecbeeb496..8cec758f5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -398,6 +398,10 @@ public abstract class AbstractCalculatedFieldProcessingService { DonAsynchron.withCallback(findFuture, existingAttributes -> { List changed = filterChangedAttr(existingAttributes, newAttributes); + if (changed.isEmpty()) { + future.set(null); + return; + } saveAttributesInternal(tenantId, entityId, cfResult, cfIds, changed, strategy, future); }, future::setException, @@ -429,7 +433,8 @@ public abstract class AbstractCalculatedFieldProcessingService { JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); Map> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts); if (tsKvMap.isEmpty()) { - future.setFuture(Futures.immediateFuture(null)); + future.set(null); + return; } List tsEntries = toTsKvEntryList(tsKvMap); TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(outputStrategy.isSaveTimeSeries(), outputStrategy.isSaveLatest(), outputStrategy.isSendWsUpdate(), outputStrategy.isProcessCfs()); From 5918bbf082750e0c1d27639edd93a25a33c50931 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 11 Nov 2025 11:51:36 +0200 Subject: [PATCH 05/14] fixed timeout and removed unnecessary abtract classes --- ...tractCalculatedFieldProcessingService.java | 26 +++++++++---------- ...faultCalculatedFieldProcessingService.java | 12 +++++---- .../AttributeImmediateOutputStrategy.java | 6 ++--- .../AttributeRuleChainOutputStrategy.java | 6 ++--- .../ImmediateOutputStrategy.java | 25 ------------------ .../data/cf/configuration/OutputStrategy.java | 6 +---- .../cf/configuration/OutputStrategyType.java | 6 ++++- .../RuleChainOutputStrategy.java | 25 ------------------ .../TimeSeriesImmediateOutputStrategy.java | 6 ++--- .../TimeSeriesRuleChainOutputStrategy.java | 6 ++--- 10 files changed, 37 insertions(+), 87 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ImmediateOutputStrategy.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 724aaa1804..6d16e11a17 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -361,21 +361,19 @@ public abstract class AbstractCalculatedFieldProcessingService { case TIME_SERIES -> saveTimeSeries(tenantId, entityId, cfResult, cfIds, System.currentTimeMillis(), future); } - if (log.isTraceEnabled()) { - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(Void v) { - callback.onSuccess(); - log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, cfResult); - } + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Void v) { + callback.onSuccess(); + log.debug("[{}][{}] Saved CF result: {}", tenantId, entityId, cfResult); + } - @Override - public void onFailure(Throwable t) { - callback.onFailure(t); - log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, cfResult, t); - } - }, MoreExecutors.directExecutor()); - } + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + log.error("[{}][{}] Failed to save CF result {}", tenantId, entityId, cfResult, t); + } + }, MoreExecutors.directExecutor()); } private void saveAttributes(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, SettableFuture future) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index f9f551f44a..b0c2b7bc38 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -96,8 +96,10 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF @Override public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { return switch (ctx.getCfType()) { - case GEOFENCING -> resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())); - case PROPAGATION -> resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); + case GEOFENCING -> + resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())); + case PROPAGATION -> + resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); default -> Collections.emptyMap(); }; } @@ -137,9 +139,9 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF } TelemetryCalculatedFieldResult telemetryResult = result instanceof TelemetryCalculatedFieldResult telemetryRes ? telemetryRes : ((PropagationCalculatedFieldResult) result).getResult(); - switch (telemetryResult.getOutputStrategy().getStrategyType()) { - case IMMEDIATE -> processImmediately(tenantId, entityId, result, cfIds, callback); - case RULE_CHAIN -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); + switch (telemetryResult.getOutputStrategy().getType()) { + case IMMEDIATE_ATTRIBUTES, IMMEDIATE_TIME_SERIES -> processImmediately(tenantId, entityId, result, cfIds, callback); + case RULE_CHAIN_ATTRIBUTES, RULE_CHAIN_TIME_SERIES -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java index 5ca28d0ee4..d4fb961894 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java @@ -22,7 +22,7 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class AttributeImmediateOutputStrategy extends ImmediateOutputStrategy { +public class AttributeImmediateOutputStrategy implements OutputStrategy { private boolean updateAttributesOnlyOnValueChange; @@ -31,7 +31,7 @@ public class AttributeImmediateOutputStrategy extends ImmediateOutputStrategy { private boolean processCfs; @Override - public String getType() { - return "IMMEDIATE_ATTRIBUTES"; + public OutputStrategyType getType() { + return OutputStrategyType.IMMEDIATE_ATTRIBUTES; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java index adefc06964..801b01401f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java @@ -20,10 +20,10 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor -public class AttributeRuleChainOutputStrategy extends RuleChainOutputStrategy { +public class AttributeRuleChainOutputStrategy implements OutputStrategy { @Override - public String getType() { - return "RULE_CHAIN_ATTRIBUTES"; + public OutputStrategyType getType() { + return OutputStrategyType.RULE_CHAIN_ATTRIBUTES; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ImmediateOutputStrategy.java deleted file mode 100644 index 170fa5bb9c..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ImmediateOutputStrategy.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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.common.data.cf.configuration; - -public abstract class ImmediateOutputStrategy implements OutputStrategy { - - @Override - public OutputStrategyType getStrategyType() { - return OutputStrategyType.IMMEDIATE; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java index 8f488602fe..21dc330da3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -32,14 +32,10 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As; @Type(value = AttributeRuleChainOutputStrategy.class, name = "RULE_CHAIN_ATTRIBUTES"), @Type(value = TimeSeriesRuleChainOutputStrategy.class, name = "RULE_CHAIN_TIME_SERIES") - }) public interface OutputStrategy { @JsonIgnore - OutputStrategyType getStrategyType(); - - @JsonIgnore - String getType(); + OutputStrategyType getType(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java index 4f5234acb5..dbabb19cae 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java @@ -17,6 +17,10 @@ package org.thingsboard.server.common.data.cf.configuration; public enum OutputStrategyType { - IMMEDIATE, RULE_CHAIN + IMMEDIATE_ATTRIBUTES, + IMMEDIATE_TIME_SERIES, + + RULE_CHAIN_ATTRIBUTES, + RULE_CHAIN_TIME_SERIES } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java deleted file mode 100644 index 0d601d22fc..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RuleChainOutputStrategy.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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.common.data.cf.configuration; - -public abstract class RuleChainOutputStrategy implements OutputStrategy { - - @Override - public OutputStrategyType getStrategyType() { - return OutputStrategyType.RULE_CHAIN; - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java index 60d5f99515..0f22a81b70 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java @@ -22,7 +22,7 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class TimeSeriesImmediateOutputStrategy extends ImmediateOutputStrategy { +public class TimeSeriesImmediateOutputStrategy implements OutputStrategy { private long ttl; @@ -32,7 +32,7 @@ public class TimeSeriesImmediateOutputStrategy extends ImmediateOutputStrategy { private boolean processCfs; @Override - public String getType() { - return "IMMEDIATE_TIME_SERIES"; + public OutputStrategyType getType() { + return OutputStrategyType.IMMEDIATE_TIME_SERIES; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java index bb50cc5e8b..d5e8caa8a1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java @@ -20,10 +20,10 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor -public class TimeSeriesRuleChainOutputStrategy extends RuleChainOutputStrategy { +public class TimeSeriesRuleChainOutputStrategy implements OutputStrategy { @Override - public String getType() { - return "RULE_CHAIN_TIME_SERIES"; + public OutputStrategyType getType() { + return OutputStrategyType.RULE_CHAIN_TIME_SERIES; } } From 558a13b5b020ce311c9d60144d55513b9cf9159a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 12 Nov 2025 10:11:53 +0200 Subject: [PATCH 06/14] refactored cf output --- .../main/data/upgrade/basic/schema_update.sql | 6 +- ...faultCalculatedFieldProcessingService.java | 10 ++- .../cf/CalculatedFieldCurrentOwnerTest.java | 6 +- .../cf/CalculatedFieldIntegrationTest.java | 65 +++++++------------ ...ntitiesAggregationCalculatedFieldTest.java | 15 ++--- .../CalculatedFieldControllerTest.java | 16 ++--- .../server/edge/CalculatedFieldEdgeTest.java | 6 +- .../GeofencingCalculatedFieldStateTest.java | 6 +- .../PropagationCalculatedFieldStateTest.java | 5 +- .../state/ScriptCalculatedFieldStateTest.java | 5 +- .../state/SimpleCalculatedFieldStateTest.java | 8 +-- .../sync/ie/ExportImportServiceSqlTest.java | 6 +- .../service/sync/vc/VersionControlTest.java | 6 +- .../AttributeImmediateOutputStrategy.java | 4 +- .../AttributeOutputStrategy.java | 33 ++++++++++ .../AttributeRuleChainOutputStrategy.java | 4 +- .../cf/configuration/AttributesOutput.java | 38 +++++++++++ .../common/data/cf/configuration/Output.java | 35 +++++++--- .../data/cf/configuration/OutputStrategy.java | 16 ----- .../cf/configuration/OutputStrategyType.java | 6 +- .../TimeSeriesImmediateOutputStrategy.java | 4 +- .../cf/configuration/TimeSeriesOutput.java | 37 +++++++++++ .../TimeSeriesOutputStrategy.java | 31 +++++++++ .../TimeSeriesRuleChainOutputStrategy.java | 4 +- .../server/dao/service/AssetServiceTest.java | 6 +- .../service/CalculatedFieldServiceTest.java | 10 ++- .../dao/service/CustomerServiceTest.java | 6 +- .../server/dao/service/DeviceServiceTest.java | 6 +- .../service/MonitoringEntityService.java | 7 +- 29 files changed, 244 insertions(+), 163 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeOutputStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 05726a92e9..9bd3d46208 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -80,11 +80,7 @@ SET configuration = jsonb_set( || jsonb_build_object( 'strategy', jsonb_build_object( - 'type', - CASE (configuration::jsonb -> 'output' ->> 'type') - WHEN 'TIME_SERIES' THEN 'RULE_CHAIN_TIME_SERIES' - WHEN 'ATTRIBUTES' THEN 'RULE_CHAIN_ATTRIBUTES' - END + 'type', 'RULE_CHAIN' ) ), false diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index b0c2b7bc38..302f51ecb1 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -96,10 +96,8 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF @Override public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { return switch (ctx.getCfType()) { - case GEOFENCING -> - resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())); - case PROPAGATION -> - resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); + case GEOFENCING -> resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())); + case PROPAGATION -> resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); default -> Collections.emptyMap(); }; } @@ -140,8 +138,8 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF TelemetryCalculatedFieldResult telemetryResult = result instanceof TelemetryCalculatedFieldResult telemetryRes ? telemetryRes : ((PropagationCalculatedFieldResult) result).getResult(); switch (telemetryResult.getOutputStrategy().getType()) { - case IMMEDIATE_ATTRIBUTES, IMMEDIATE_TIME_SERIES -> processImmediately(tenantId, entityId, result, cfIds, callback); - case RULE_CHAIN_ATTRIBUTES, RULE_CHAIN_TIME_SERIES -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); + case IMMEDIATE -> processImmediately(tenantId, entityId, result, cfIds, callback); + case RULE_CHAIN -> pushMsgToRuleEngine(tenantId, entityId, result, cfIds, callback); } } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java index d2f9621064..6c6401f088 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldCurrentOwnerTest.java @@ -26,10 +26,9 @@ 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.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CurrentOwnerDynamicSourceConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.controller.AbstractControllerTest; @@ -175,9 +174,8 @@ public class CalculatedFieldCurrentOwnerTest extends AbstractControllerTest { config.setExpression("a + 100"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("result"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(0); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 8593ea8ae5..1d535a96c2 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -33,15 +33,15 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -94,13 +94,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); - calculatedField.setVersion(1L); CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); @@ -122,10 +120,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); }); - Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); - savedOutput.setType(OutputType.ATTRIBUTES); - savedOutput.setScope(AttributeScope.SERVER_SCOPE); - savedOutput.setName("temperatureF"); + AttributesOutput newOutput = new AttributesOutput(); + newOutput.setScope(AttributeScope.SERVER_SCOPE); + newOutput.setName("temperatureF"); + config.setOutput(newOutput); + savedCalculatedField.setConfiguration(config); + savedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -180,9 +180,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); @@ -229,9 +228,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); @@ -293,9 +291,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setExpression("x + y"); - Output output = new Output(); + AttributesOutput output = new AttributesOutput(); output.setName("z"); - output.setType(OutputType.ATTRIBUTES); output.setScope(AttributeScope.SERVER_SCOPE); config.setOutput(output); @@ -450,9 +447,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/0) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); calculatedField.setConfiguration(config); @@ -500,9 +496,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); config.setUseLatestTs(true); @@ -550,9 +545,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("a", argument1, "b", argument2)); config.setExpression("a + b"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("c"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); config.setUseLatestTs(true); @@ -604,9 +598,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("return {\"ts\": ctx.latestTs, \"values\": {\"fahrenheitTemp\": (T * 1.8) + 32}};"); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); calculatedField.setConfiguration(config); @@ -640,9 +632,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("m", argument)); config.setExpression("m + 1"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("m1"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(0); config.setOutput(output); @@ -715,8 +706,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); // Output to server attributes - Output out = new Output(); - out.setType(OutputType.ATTRIBUTES); + AttributesOutput out = new AttributesOutput(); out.setScope(AttributeScope.SERVER_SCOPE); cfg.setOutput(out); @@ -826,8 +816,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); // Output to server attributes - Output out = new Output(); - out.setType(OutputType.ATTRIBUTES); + AttributesOutput out = new AttributesOutput(); out.setScope(AttributeScope.SERVER_SCOPE); cfg.setOutput(out); @@ -924,8 +913,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup)); // Server attributes output - Output out = new Output(); - out.setType(OutputType.ATTRIBUTES); + AttributesOutput out = new AttributesOutput(); out.setScope(AttributeScope.SERVER_SCOPE); cfg.setOutput(out); @@ -1035,8 +1023,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setExpression("{\"testResult\": t * 2}"); - Output output = new Output(); - output.setType(OutputType.ATTRIBUTES); + AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); cfg.setOutput(output); @@ -1112,9 +1099,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); cfg.setArguments(Map.of("temperatureComputed", arg)); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - cfg.setOutput(output); + cfg.setOutput(new TimeSeriesOutput()); cf.setConfiguration(cfg); @@ -1182,9 +1167,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("a", argumentA, "b", argumentB)); config.setExpression("a + b"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("c"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(0); config.setOutput(output); @@ -1231,9 +1215,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes config.setArguments(Map.of("T", argument)); config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(1); output.setStrategy(new TimeSeriesImmediateOutputStrategy(1000L, true, true, true, true)); diff --git a/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java index 48c4e67608..3739cc2618 100644 --- a/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/RelatedEntitiesAggregationCalculatedFieldTest.java @@ -32,9 +32,10 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; @@ -626,8 +627,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr }); var configuration = (RelatedEntitiesAggregationCalculatedFieldConfiguration) cf.getConfiguration(); - Output output = new Output(); - output.setType(OutputType.ATTRIBUTES); + AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); configuration.setOutput(output); saveCalculatedField(cf); @@ -702,8 +702,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr avgMetric.setInput(new AggKeyInput("temp")); aggMetrics.put("avgTemperature", avgMetric); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setDecimalsByDefault(0); return createAggCf("Average temperature", entityId, @@ -739,8 +738,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr totalSpaces.setInput(new AggFunctionInput("return 1;")); aggMetrics.put("totalSpaces", totalSpaces); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setDecimalsByDefault(0); return createAggCf("Occupied spaces", entityId, @@ -776,8 +774,7 @@ public class RelatedEntitiesAggregationCalculatedFieldTest extends AbstractContr totalSpaces.setInput(new AggFunctionInput("return 1;")); aggMetrics.put("totalSpaces", totalSpaces); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setDecimalsByDefault(0); return createAggCf("Occupied spaces", entityId, diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 4ebace6ae7..b99b416c10 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -26,12 +26,11 @@ 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.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -251,13 +250,10 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude")); config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration)); config.setScheduledUpdateEnabled(false); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); return config; } @@ -275,10 +271,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { config.setApplyExpressionToResolvedArguments(false); config.setExpression(null); - - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); Argument arg = new Argument(); arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); @@ -299,9 +292,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java index 268e19345c..4161ff845d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/CalculatedFieldEdgeTest.java @@ -26,10 +26,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -224,9 +223,8 @@ public class CalculatedFieldEdgeTest extends AbstractEdgeTest { config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(2); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index d144fe4dcc..6b846b3b15 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -29,8 +29,8 @@ 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; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; @@ -476,9 +476,7 @@ public class GeofencingCalculatedFieldStateTest { config.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); return config; } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index ddb9f378b0..d5546f4fea 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -31,8 +31,8 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -235,8 +235,7 @@ public class PropagationCalculatedFieldStateTest { config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg)); config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}"); - Output output = new Output(); - output.setType(OutputType.ATTRIBUTES); + AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 51e633a232..96ca21c769 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -31,9 +31,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; @@ -221,8 +221,7 @@ public class ScriptCalculatedFieldStateTest { config.setExpression("return {\"maxDeviceTemperature\": deviceTemperature.max(), \"assetHumidity\": assetHumidity / 2 }"); - Output output = new Output(); - output.setType(OutputType.ATTRIBUTES); + AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index a64f1e4c60..79b05a1e41 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -28,11 +28,12 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -189,7 +190,7 @@ public class SimpleCalculatedFieldStateTest { "key3", new SingleValueArgumentEntry(System.currentTimeMillis() - 3, new DoubleDataEntry("key3", 23.1), 184L) )); - Output output = getCalculatedFieldConfig().getOutput(); + TimeSeriesOutput output = (TimeSeriesOutput) getCalculatedFieldConfig().getOutput(); output.setDecimalsByDefault(3); ctx.setOutput(output); @@ -263,9 +264,8 @@ public class SimpleCalculatedFieldStateTest { config.setExpression("key1 + key2 + key3"); - Output output = new Output(); + AttributesOutput output = new AttributesOutput(); output.setName("output"); - output.setType(OutputType.ATTRIBUTES); output.setScope(AttributeScope.SERVER_SCOPE); output.setDecimalsByDefault(0); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index e70a0cd37c..70711de64d 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -48,10 +48,9 @@ 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.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; @@ -627,9 +626,8 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 2db78871d4..2f2a584432 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -52,10 +52,9 @@ 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.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; import org.thingsboard.server.common.data.device.data.DeviceData; @@ -1148,9 +1147,8 @@ public class VersionControlTest extends AbstractControllerTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java index d4fb961894..737c4fc64e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java @@ -22,7 +22,7 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class AttributeImmediateOutputStrategy implements OutputStrategy { +public class AttributeImmediateOutputStrategy implements AttributeOutputStrategy { private boolean updateAttributesOnlyOnValueChange; @@ -32,6 +32,6 @@ public class AttributeImmediateOutputStrategy implements OutputStrategy { @Override public OutputStrategyType getType() { - return OutputStrategyType.IMMEDIATE_ATTRIBUTES; + return OutputStrategyType.IMMEDIATE; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeOutputStrategy.java new file mode 100644 index 0000000000..f47bc27579 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeOutputStrategy.java @@ -0,0 +1,33 @@ +/** + * 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.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AttributeImmediateOutputStrategy.class, name = "IMMEDIATE"), + @JsonSubTypes.Type(value = AttributeRuleChainOutputStrategy.class, name = "RULE_CHAIN"), +}) +public interface AttributeOutputStrategy extends OutputStrategy { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java index 801b01401f..ce03aeb750 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java @@ -20,10 +20,10 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor -public class AttributeRuleChainOutputStrategy implements OutputStrategy { +public class AttributeRuleChainOutputStrategy implements AttributeOutputStrategy { @Override public OutputStrategyType getType() { - return OutputStrategyType.RULE_CHAIN_ATTRIBUTES; + return OutputStrategyType.RULE_CHAIN; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java new file mode 100644 index 0000000000..61195fc5f4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java @@ -0,0 +1,38 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.Data; +import org.thingsboard.server.common.data.AttributeScope; + +@Data +public class AttributesOutput implements Output { + + private String name; + private AttributeScope scope; + private Integer decimalsByDefault; + + private AttributeOutputStrategy strategy; + + public AttributesOutput() { + this.strategy = new AttributeRuleChainOutputStrategy(); + } + + @Override + public OutputType getType() { + return OutputType.ATTRIBUTES; + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index d2e0ac1ca9..4658d41221 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -15,18 +15,37 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.AttributeScope; -@Data +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TimeSeriesOutput.class, name = "TIME_SERIES"), + @JsonSubTypes.Type(value = AttributesOutput.class, name = "ATTRIBUTES") +}) @JsonInclude(JsonInclude.Include.NON_NULL) -public class Output { +@JsonIgnoreProperties(ignoreUnknown = true) +public interface Output { - private String name; - private OutputType type; - private AttributeScope scope; - private Integer decimalsByDefault; - private OutputStrategy strategy; + @JsonIgnore + OutputType getType(); + + String getName(); + + OutputStrategy getStrategy(); + + default AttributeScope getScope() { + return null; + } + + Integer getDecimalsByDefault(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java index 21dc330da3..b635047ae3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategy.java @@ -16,23 +16,7 @@ package org.thingsboard.server.common.data.cf.configuration; 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.fasterxml.jackson.annotation.JsonTypeInfo.As; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = As.PROPERTY, - property = "type" -) -@JsonSubTypes({ - @Type(value = AttributeImmediateOutputStrategy.class, name = "IMMEDIATE_ATTRIBUTES"), - @Type(value = TimeSeriesImmediateOutputStrategy.class, name = "IMMEDIATE_TIME_SERIES"), - - @Type(value = AttributeRuleChainOutputStrategy.class, name = "RULE_CHAIN_ATTRIBUTES"), - @Type(value = TimeSeriesRuleChainOutputStrategy.class, name = "RULE_CHAIN_TIME_SERIES") -}) public interface OutputStrategy { @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java index dbabb19cae..4f5234acb5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/OutputStrategyType.java @@ -17,10 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration; public enum OutputStrategyType { - IMMEDIATE_ATTRIBUTES, - IMMEDIATE_TIME_SERIES, - - RULE_CHAIN_ATTRIBUTES, - RULE_CHAIN_TIME_SERIES + IMMEDIATE, RULE_CHAIN } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java index 0f22a81b70..31b15095d4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesImmediateOutputStrategy.java @@ -22,7 +22,7 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class TimeSeriesImmediateOutputStrategy implements OutputStrategy { +public class TimeSeriesImmediateOutputStrategy implements TimeSeriesOutputStrategy { private long ttl; @@ -33,6 +33,6 @@ public class TimeSeriesImmediateOutputStrategy implements OutputStrategy { @Override public OutputStrategyType getType() { - return OutputStrategyType.IMMEDIATE_TIME_SERIES; + return OutputStrategyType.IMMEDIATE; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java new file mode 100644 index 0000000000..5c6a907290 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutput.java @@ -0,0 +1,37 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.Data; + +@Data +public class TimeSeriesOutput implements Output { + + private String name; + private Integer decimalsByDefault; + + private TimeSeriesOutputStrategy strategy; + + public TimeSeriesOutput() { + this.strategy = new TimeSeriesRuleChainOutputStrategy(); + } + + @Override + public OutputType getType() { + return OutputType.TIME_SERIES; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java new file mode 100644 index 0000000000..303c180c46 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesOutputStrategy.java @@ -0,0 +1,31 @@ +/** + * 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.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = TimeSeriesImmediateOutputStrategy.class, name = "IMMEDIATE"), + @JsonSubTypes.Type(value = TimeSeriesRuleChainOutputStrategy.class, name = "RULE_CHAIN") +}) +public interface TimeSeriesOutputStrategy extends OutputStrategy { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java index d5e8caa8a1..7786efff1a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/TimeSeriesRuleChainOutputStrategy.java @@ -20,10 +20,10 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor -public class TimeSeriesRuleChainOutputStrategy implements OutputStrategy { +public class TimeSeriesRuleChainOutputStrategy implements TimeSeriesOutputStrategy { @Override public OutputStrategyType getType() { - return OutputStrategyType.RULE_CHAIN_TIME_SERIES; + return OutputStrategyType.RULE_CHAIN; } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index 462e7a894c..7bea8f6f74 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -34,10 +34,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -894,9 +893,8 @@ public class AssetServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 0e20f188b1..d06a7d6355 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -28,11 +28,10 @@ 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.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -163,7 +162,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { .getMaxRelationLevelPerCfArgument(); // Zone-group argument (ATTRIBUTE) - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); List levels = new ArrayList<>(); @@ -203,7 +202,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); @@ -300,9 +299,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java index daa10e72e9..e69685f429 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java @@ -35,10 +35,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -389,9 +388,8 @@ public class CustomerServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 32767043d7..25e813aac4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -43,10 +43,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; @@ -1243,9 +1242,8 @@ public class DeviceServiceTest extends AbstractServiceTest { config.setExpression("T - (100 - H) / 5"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("output"); - output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index 062104ecd5..d2cf076516 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -40,10 +40,9 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; @@ -241,9 +240,7 @@ public class MonitoringEntityService { TEST_TELEMETRY_KEY, testDataArgument )); configuration.setExpression("return { \"" + TEST_CF_TELEMETRY_KEY + "\": " + TEST_TELEMETRY_KEY + " + \"-cf\" };"); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - configuration.setOutput(output); + configuration.setOutput(new TimeSeriesOutput()); calculatedField.setConfiguration(configuration); calculatedField.setDebugMode(true); tbClient.saveCalculatedField(calculatedField); From 7a13183d5b3638d5c5511c421c6ae8091c196ec0 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 12 Nov 2025 19:06:42 +0200 Subject: [PATCH 07/14] UI: Add CF output strategy --- .../calculated-field-dialog.component.ts | 10 ++- .../geofencing-configuration.component.ts | 6 +- .../calculated-field-output.component.html | 83 +++++++++++++++++++ .../calculated-field-output.component.scss | 7 ++ .../calculated-field-output.component.ts | 58 ++++++++++++- .../propagation-configuration.component.ts | 5 +- ...ntities-aggregation-component.component.ts | 5 +- .../simple-configuration.component.ts | 7 +- .../shared/models/calculated-field.models.ts | 64 ++++++++++++++ .../assets/locale/locale.constant-en_US.json | 22 +++++ 10 files changed, 249 insertions(+), 18 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 17c550dcdf..7174ec9dd9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -26,7 +26,8 @@ import { CalculatedFieldConfiguration, CalculatedFieldTestScriptFn, CalculatedFieldType, - CalculatedFieldTypeTranslations + CalculatedFieldTypeTranslations, + OutputStrategyType } from '@shared/models/calculated-field.models'; import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { EntityType } from '@shared/models/entity-type.models'; @@ -36,7 +37,7 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; -import { deepTrim } from '@core/utils'; +import { deepTrim, isDefined } from '@core/utils'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -121,6 +122,11 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts index 846b063042..3406922b9e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts @@ -30,9 +30,9 @@ import { CalculatedFieldGeofencingConfiguration, CalculatedFieldOutput, CalculatedFieldType, + defaultCalculatedFieldOutput, getCalculatedFieldCurrentEntityFilter, - notEmptyObjectValidator, - OutputType + notEmptyObjectValidator } from '@shared/models/calculated-field.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; @@ -83,7 +83,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V zoneGroups: this.fb.control>({}, notEmptyObjectValidator()), scheduledUpdateEnabled: [true], scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], - output: this.fb.control({type: OutputType.Timeseries}) + output: this.fb.control(defaultCalculatedFieldOutput) }); currentEntityFilter: EntityFilter; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index 4d8515ac7e..144a8384ab 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -78,6 +78,89 @@ } } +
+
+
+ {{ 'calculated-fields.output-strategy.strategy' | translate }} +
+ + @for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) { + {{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }} + } + +
+ @if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) { +
+
+ {{ 'calculated-fields.output-strategy.processing-options' | translate }} +
+ + @if (outputForm.get('type').value === OutputType.Timeseries) { + + {{ 'calculated-fields.output-strategy.save-time-series' | translate }} + + + {{ 'calculated-fields.output-strategy.save-latest-values' | translate }} + + } @else { + + {{ 'calculated-fields.output-strategy.save-database' | translate }} + + } + + {{ 'calculated-fields.output-strategy.send-web-sockets' | translate }} + + + {{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }} + + +
+ @if (outputForm.get('type').value === OutputType.Attribute) { +
+ +
+
calculated-fields.output-strategy.update-attributes-only-on-value-change
+
+
+
+ } @else { + + + help_outline + + + } + } +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss new file mode 100644 index 0000000000..e0a24dc86e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss @@ -0,0 +1,7 @@ +:host ::ng-deep { + .mat-mdc-chip-disabled { + .mdc-evolution-chip__action { + cursor: default + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 2a9215b7cf..a3d575af90 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -28,6 +28,8 @@ import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { CalculatedFieldOutput, CalculatedFieldSimpleOutput, + OutputStrategyType, + OutputStrategyTypeTranslations, OutputType, OutputTypeTranslations } from '@shared/models/calculated-field.models'; @@ -52,6 +54,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; multi: true } ], + styleUrls: ['./calculated-field-output.component.scss'], }) export class CalculatedFieldOutputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { @@ -72,6 +75,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val readonly OutputTypeTranslations = OutputTypeTranslations; readonly EntityType = EntityType; + readonly OutputStrategyType = OutputStrategyType; + readonly OutputStrategyTypes = Object.values(OutputStrategyType) as OutputStrategyType[]; + readonly OutputStrategyTypeTranslations = OutputStrategyTypeTranslations; + private fb = inject(FormBuilder); private destroyRef = inject(DestroyRef); @@ -80,6 +87,16 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val scope: [{value: AttributeScope.SERVER_SCOPE, disabled: true}], type: [OutputType.Timeseries], decimalsByDefault: [null as number, [Validators.min(0), Validators.max(15), Validators.pattern(digitsRegex)]], + strategy: this.fb.group({ + type: [OutputStrategyType.IMMEDIATE], + saveTimeSeries: [true], + saveLatest: [true], + saveAttribute: [true], + sendWsUpdate: [true], + processCfs: [true], + updateAttributesOnlyOnValueChange: [true], + ttl: [0] + }) }); private propagateChange: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void = () => { }; @@ -87,14 +104,23 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val ngOnInit() { this.outputForm.get('type').valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(type => this.toggleScopeByOutputType(type)); + .subscribe(type => { + this.toggleScopeByOutputType(type); + this.updatedStrategy(); + }); + + this.outputForm.get('strategy.type').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.updatedStrategy(); + }); this.updatedFormWithMode(); this.outputForm.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => { - this.updatedModel(value) + this.updatedModel(value); }) } @@ -134,6 +160,14 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.outputForm.enable({emitEvent: false}); this.updatedFormWithMode(); this.toggleScopeByOutputType(this.outputForm.get('type').value); + this.updatedStrategy(); + } + } + + toggleChip(controlName: string) { + const control = this.outputForm.get('strategy').get(controlName); + if (control && control.enabled) { + control.setValue(!control.value); } } @@ -164,4 +198,24 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.outputForm.get('decimalsByDefault').disable({emitEvent: false}); } } + + private updatedStrategy(): void { + const strategyType = this.outputForm.get('strategy.type').value; + this.outputForm.get('strategy').disable({emitEvent: false}); + this.outputForm.get('strategy.type').enable({emitEvent: false}); + + if (strategyType === OutputStrategyType.IMMEDIATE) { + const outputType = this.outputForm.get('type').value; + this.outputForm.get('strategy.sendWsUpdate').enable({emitEvent: false}); + this.outputForm.get('strategy.processCfs').enable({emitEvent: false}); + if (outputType === OutputType.Attribute) { + this.outputForm.get('strategy.saveAttribute').enable({emitEvent: false}); + this.outputForm.get('strategy.updateAttributesOnlyOnValueChange').enable({emitEvent: false}); + } else { + this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false}); + this.outputForm.get('strategy.saveLatest').enable({emitEvent: false}); + this.outputForm.get('strategy.ttl').enable({emitEvent: false}); + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index 4fb8bb561e..0a69c59568 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -31,6 +31,7 @@ import { CalculatedFieldOutput, CalculatedFieldPropagationConfiguration, CalculatedFieldType, + defaultCalculatedFieldOutput, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, notEmptyObjectValidator, @@ -81,9 +82,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, relationType: ['Contains', Validators.required], }), expression: [calculatedFieldDefaultScript], - output: this.fb.control({ - type: OutputType.Timeseries, - }), + output: this.fb.control(defaultCalculatedFieldOutput), }); readonly ScriptLanguage = ScriptLanguage; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts index c53f400916..731330ec76 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts @@ -30,6 +30,7 @@ import { CalculatedFieldOutput, CalculatedFieldRelatedAggregationConfiguration, CalculatedFieldType, + defaultCalculatedFieldOutput, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, notEmptyObjectValidator, @@ -87,9 +88,7 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu arguments: this.fb.control({}, notEmptyObjectValidator()), metrics: this.fb.control({}, notEmptyObjectValidator()), deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF], - output: this.fb.control({ - type: OutputType.Timeseries, - }), + output: this.fb.control(defaultCalculatedFieldOutput), useLatestTs: [false] }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index 065556081c..0c70361e5e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -31,6 +31,7 @@ import { CalculatedFieldSimpleConfiguration, CalculatedFieldSimpleOutput, CalculatedFieldType, + defaultSimpleCalculatedFieldOutput, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, OutputType @@ -84,11 +85,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid arguments: this.fb.control({}), expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], expressionSCRIPT: [calculatedFieldDefaultScript], - output: this.fb.control({ - name: '', - type: OutputType.Timeseries, - decimalsByDefault: null - }), + output: this.fb.control(defaultSimpleCalculatedFieldOutput), useLatestTs: [false] }); diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 1b43700ebe..e08066066c 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -185,10 +185,12 @@ export type CalculatedFieldOutput = export interface CalculatedFieldOutputAttribute { type: OutputType.Attribute, scope: AttributeScope; + strategy: AttributeOutputStrategy; } export interface CalculatedFieldOutputTimeSeries { type: OutputType.Timeseries; + strategy: TimeSeriesOutputStrategy; } export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { @@ -196,6 +198,51 @@ export type CalculatedFieldSimpleOutput = CalculatedFieldOutput & { decimalsByDefault?: number; } +export type AttributeOutputStrategy = + | AttributeImmediateOutputStrategy + | AttributeRuleChainOutputStrategy; + +export interface AttributeImmediateOutputStrategy { + type: OutputStrategyType.IMMEDIATE; + updateAttributesOnlyOnValueChange: boolean; + saveAttribute: boolean; + sendWsUpdate: boolean; + processCfs: boolean; +} + +export interface AttributeRuleChainOutputStrategy { + type: OutputStrategyType.RULE_CHAIN; +} + +export type TimeSeriesOutputStrategy = + | TimeSeriesRuleChainOutputStrategy + | TimeSeriesImmediateOutputStrategy; + +export interface TimeSeriesRuleChainOutputStrategy { + type: OutputStrategyType.IMMEDIATE; + ttl: number; + saveTimeSeries: boolean; + saveLatest: boolean; + sendWsUpdate: boolean; + processCfs: boolean; +} + +export interface TimeSeriesImmediateOutputStrategy { + type: OutputStrategyType.RULE_CHAIN; +} + +export enum OutputStrategyType { + IMMEDIATE = 'IMMEDIATE', + RULE_CHAIN = 'RULE_CHAIN' +} + +export const OutputStrategyTypeTranslations = new Map( + [ + [OutputStrategyType.IMMEDIATE, 'calculated-fields.output-strategy.process-right-away'], + [OutputStrategyType.RULE_CHAIN, 'calculated-fields.output-strategy.process-rule-chains'], + ] +) + export enum ArgumentEntityType { Current = 'CURRENT', Device = 'DEVICE', @@ -458,6 +505,23 @@ export type CalculatedFieldArgumentEventValue = CalculatedF export type CalculatedFieldEventArguments = Record>; +export const defaultCalculatedFieldOutput: CalculatedFieldOutputTimeSeries = { + type: OutputType.Timeseries, + strategy: { + type: OutputStrategyType.IMMEDIATE, + ttl: 0, + saveTimeSeries: true, + saveLatest: true, + sendWsUpdate: true, + processCfs: true + } +} + +export const defaultSimpleCalculatedFieldOutput: CalculatedFieldSimpleOutput = { + name: '', + ...defaultCalculatedFieldOutput +} + export const CalculatedFieldCtxLatestTelemetryArgumentAutocomplete = { meta: 'object', type: '{ ts: number; value: any; }', diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0c501f5c5e..b827ead50b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1193,6 +1193,28 @@ "filter": "Filter", "filter-hint": "Enables filtering of entities during aggregation. The filter function must return a boolean value and can use all configured arguments." }, + "output-strategy": { + "strategy": "Strategy", + "process-right-away": "Process right away", + "process-rule-chains": "Process via Rule Chains", + "processing-options": "Processing options", + "save-time-series": "Save to time series", + "save-database": "Save to database", + "save-latest-values": "Save to latest values", + "send-web-sockets": "Send to WebSockets", + "save-calculated-fields": "Send to Calculated fields", + "update-attributes-only-on-value-change": "Save attributes only if the value changes", + "ttl": "TTL", + "ttl-required": "TTL is required.", + "ttl-min": "Only 0 minimum TTL is allowed.", + "hint": { + "strategy": "Strategy", + "processing-options": "Processing options", + "update-attributes-only-on-value-change": "Updates the attributes on every incoming message disregarding if their value has changed. Increases API usage and reduces performance.", + "update-attributes-only-on-value-change-enabled": "Updates the attributes only if their value has changed. If the value is not changed, no update to the attribute timestamp nor attribute change notification will be sent.", + "ttl": "If no value is present, it defaults to the TTL specified in the configuration. If the value is set to 0, the TTL from the tenant profile configuration will be applied." + } + }, "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.", From 9c564dc0d90378168ea2b9d08aba9445f0ceb85a Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 14 Nov 2025 09:07:22 +0200 Subject: [PATCH 08/14] fixed test --- .../service/cf/ctx/state/SimpleCalculatedFieldStateTest.java | 3 +-- .../server/common/data/cf/configuration/Output.java | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 79b05a1e41..0568f899d7 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -190,7 +189,7 @@ public class SimpleCalculatedFieldStateTest { "key3", new SingleValueArgumentEntry(System.currentTimeMillis() - 3, new DoubleDataEntry("key3", 23.1), 184L) )); - TimeSeriesOutput output = (TimeSeriesOutput) getCalculatedFieldConfig().getOutput(); + Output output = getCalculatedFieldConfig().getOutput(); output.setDecimalsByDefault(3); ctx.setOutput(output); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java index 4658d41221..848ad759e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Output.java @@ -48,4 +48,6 @@ public interface Output { Integer getDecimalsByDefault(); + void setDecimalsByDefault(Integer decimalsByDefault); + } From d59de750d0f5b54962436cbf8726a73f60867848 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 14 Nov 2025 10:35:08 +0200 Subject: [PATCH 09/14] refactoring --- ...tractCalculatedFieldProcessingService.java | 89 ++++++++++--------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 6d16e11a17..39ab78f86a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -33,10 +33,12 @@ import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; import org.thingsboard.server.common.adaptor.JsonConverter; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedField; 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.AttributeImmediateOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.TimeSeriesImmediateOutputStrategy; @@ -357,8 +359,8 @@ public abstract class AbstractCalculatedFieldProcessingService { SettableFuture future = SettableFuture.create(); switch (type) { - case ATTRIBUTES -> saveAttributes(tenantId, entityId, cfResult, cfIds, future); - case TIME_SERIES -> saveTimeSeries(tenantId, entityId, cfResult, cfIds, System.currentTimeMillis(), future); + case ATTRIBUTES -> saveAttributes(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfResult.getScope(), cfIds, future); + case TIME_SERIES -> saveTimeSeries(tenantId, entityId, jsonResult, cfResult.getOutputStrategy(), cfIds, System.currentTimeMillis(), future); } Futures.addCallback(future, new FutureCallback<>() { @@ -376,39 +378,37 @@ public abstract class AbstractCalculatedFieldProcessingService { }, MoreExecutors.directExecutor()); } - private void saveAttributes(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, SettableFuture future) { - if (!(cfResult.getOutputStrategy() instanceof AttributeImmediateOutputStrategy outputStrategy)) { - future.setException(new IllegalArgumentException("Expected AttributeImmediateOutputStrategy")); - return; - } - JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); + private void saveAttributes(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, AttributeScope scope, List cfIds, SettableFuture future) { + if (!(outputStrategy instanceof AttributeImmediateOutputStrategy attOutputStrategy)) { + future.setException(new IllegalArgumentException("Only AttributeImmediateOutputStrategy is supported.")); + } else { + AttributesSaveRequest.Strategy strategy = new Strategy(attOutputStrategy.isSaveAttribute(), attOutputStrategy.isSendWsUpdate(), attOutputStrategy.isProcessCfs()); + List newAttributes = JsonConverter.convertToAttributes(jsonResult); - AttributesSaveRequest.Strategy strategy = new Strategy(outputStrategy.isSaveAttribute(), outputStrategy.isSendWsUpdate(), outputStrategy.isProcessCfs()); - List newAttributes = JsonConverter.convertToAttributes(jsonResult); + if (!attOutputStrategy.isUpdateAttributesOnlyOnValueChange()) { + saveAttributesInternal(tenantId, entityId, scope, cfIds, newAttributes, strategy, future); + return; + } - if (!outputStrategy.isUpdateAttributesOnlyOnValueChange()) { - saveAttributesInternal(tenantId, entityId, cfResult, cfIds, newAttributes, strategy, future); - return; - } + List keys = newAttributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); + ListenableFuture> findFuture = attributesService.find(tenantId, entityId, scope, keys); - List keys = newAttributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); - ListenableFuture> findFuture = attributesService.find(tenantId, entityId, cfResult.getScope(), keys); - - DonAsynchron.withCallback(findFuture, - existingAttributes -> { - List changed = filterChangedAttr(existingAttributes, newAttributes); - if (changed.isEmpty()) { - future.set(null); - return; - } - saveAttributesInternal(tenantId, entityId, cfResult, cfIds, changed, strategy, future); - }, - future::setException, - MoreExecutors.directExecutor()); + DonAsynchron.withCallback(findFuture, + existingAttributes -> { + List changed = filterChangedAttr(existingAttributes, newAttributes); + if (changed.isEmpty()) { + future.set(null); + return; + } + saveAttributesInternal(tenantId, entityId, scope, cfIds, changed, strategy, future); + }, + future::setException, + MoreExecutors.directExecutor()); + } } private void saveAttributesInternal(TenantId tenantId, EntityId entityId, - TelemetryCalculatedFieldResult cfResult, + AttributeScope scope, List cfIds, List entries, AttributesSaveRequest.Strategy strategy, @@ -416,7 +416,7 @@ public abstract class AbstractCalculatedFieldProcessingService { tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() .tenantId(tenantId) .entityId(entityId) - .scope(cfResult.getScope()) + .scope(scope) .entries(entries) .strategy(strategy) .previousCalculatedFieldIds(cfIds) @@ -424,28 +424,35 @@ public abstract class AbstractCalculatedFieldProcessingService { .build()); } - private void saveTimeSeries(TenantId tenantId, EntityId entityId, TelemetryCalculatedFieldResult cfResult, List cfIds, long ts, SettableFuture future) { - if (!(cfResult.getOutputStrategy() instanceof TimeSeriesImmediateOutputStrategy outputStrategy)) { - future.setException(new IllegalArgumentException("Expected TimeSeriesImmediateOutputStrategy")); - return; + private void saveTimeSeries(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, List cfIds, long ts, SettableFuture future) { + if (!(outputStrategy instanceof TimeSeriesImmediateOutputStrategy tsOutputStrategy)) { + future.setException(new IllegalArgumentException("Only TimeSeriesImmediateOutputStrategy is supported.")); + } else { + TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(tsOutputStrategy.isSaveTimeSeries(), tsOutputStrategy.isSaveLatest(), tsOutputStrategy.isSendWsUpdate(), tsOutputStrategy.isProcessCfs()); + saveTimeSeriesInternal(tenantId, entityId, jsonResult, tsOutputStrategy.getTtl(), cfIds, ts, strategy, future); } - JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); + } + + private void saveTimeSeriesInternal(TenantId tenantId, EntityId entityId, JsonElement jsonResult, Long ttl, List cfIds, long ts, TimeseriesSaveRequest.Strategy strategy, SettableFuture future) { Map> tsKvMap = JsonConverter.convertToTelemetry(jsonResult, ts); if (tsKvMap.isEmpty()) { future.set(null); return; } List tsEntries = toTsKvEntryList(tsKvMap); - TimeseriesSaveRequest.Strategy strategy = new TimeseriesSaveRequest.Strategy(outputStrategy.isSaveTimeSeries(), outputStrategy.isSaveLatest(), outputStrategy.isSendWsUpdate(), outputStrategy.isProcessCfs()); - tsSubService.saveTimeseriesInternal(TimeseriesSaveRequest.builder() + TimeseriesSaveRequest.Builder builder = TimeseriesSaveRequest.builder() .tenantId(tenantId) .entityId(entityId) .entries(tsEntries) - .ttl(outputStrategy.getTtl()) .strategy(strategy) - .previousCalculatedFieldIds(cfIds) - .future(future) - .build()); + .future(future); + if (ttl != null) { + builder.ttl(ttl); + } + if (cfIds != null && !cfIds.isEmpty()) { + builder.previousCalculatedFieldIds(cfIds); + } + tsSubService.saveTimeseriesInternal(builder.build()); } } From 61f06a0b9b44583c8e6374195afb28fe5b060547 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 14 Nov 2025 16:33:33 +0200 Subject: [PATCH 10/14] fixed tests --- .../RelatedEntitiesAggregationCalculatedFieldState.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index ff50cd99b6..0aaebdfc80 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -166,6 +166,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat lastMetricsEvalTs = System.currentTimeMillis(); scheduleReevaluation(); return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) .type(output.getType()) .scope(output.getScope()) .result(toSimpleResult(ctx.isUseLatestTs(), aggResult)) From 4e4dfdfe6577f37e1fa583f5379700ba665d96fd Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Mon, 17 Nov 2025 08:56:04 +0200 Subject: [PATCH 11/14] fixed compilation error in tests --- .../server/msa/cf/CalculatedFieldTest.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 9c043eee8d..481a85e3f7 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -31,13 +31,13 @@ import org.thingsboard.server.common.data.cf.CalculatedField; 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.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.AttributesOutput; import org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -183,10 +183,11 @@ public class CalculatedFieldTest extends AbstractContainerTest { CalculatedField savedCalculatedField = createSimpleCalculatedField(); - Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); - savedOutput.setType(OutputType.ATTRIBUTES); - savedOutput.setScope(SERVER_SCOPE); - savedOutput.setName("temperatureF"); + AttributesOutput output = new AttributesOutput(); + output.setScope(SERVER_SCOPE); + output.setName("temperatureF"); + ((SimpleCalculatedFieldConfiguration) savedCalculatedField.getConfiguration()).setOutput(output); + testRestClient.postCalculatedField(savedCalculatedField); await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) @@ -381,8 +382,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { cfg.setZoneGroups(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); - Output out = new Output(); - out.setType(OutputType.ATTRIBUTES); + AttributesOutput out = new AttributesOutput(); out.setScope(SERVER_SCOPE); cfg.setOutput(out); cf.setConfiguration(cfg); @@ -458,8 +458,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { cfg.setExpression("{\"testResult\": t * 2}"); - Output output = new Output(); - output.setType(OutputType.ATTRIBUTES); + AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); cfg.setOutput(output); @@ -541,9 +540,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); cfg.setArguments(Map.of("temperatureComputed", arg)); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - cfg.setOutput(output); + cfg.setOutput(new TimeSeriesOutput()); cf.setConfiguration(cfg); @@ -613,9 +610,8 @@ public class CalculatedFieldTest extends AbstractContainerTest { config.setExpression("(T * 9/5) + 32"); - Output output = new Output(); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setName("fahrenheitTemp"); - output.setType(OutputType.TIME_SERIES); output.setDecimalsByDefault(2); config.setOutput(output); @@ -647,9 +643,7 @@ public class CalculatedFieldTest extends AbstractContainerTest { config.setExpression(exampleScript); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - config.setOutput(output); + config.setOutput(new TimeSeriesOutput()); calculatedField.setConfiguration(config); From ce92740f1f1ad99b08b1b7229e47510539af9884 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 18 Nov 2025 10:51:54 +0200 Subject: [PATCH 12/14] review fixes --- ...tractCalculatedFieldProcessingService.java | 12 ++-- .../cf/CalculatedFieldProcessingService.java | 4 -- ...faultCalculatedFieldProcessingService.java | 6 +- ...=> AttributesImmediateOutputStrategy.java} | 2 +- .../cf/configuration/AttributesOutput.java | 4 +- ...egy.java => AttributesOutputStrategy.java} | 6 +- ...=> AttributesRuleChainOutputStrategy.java} | 2 +- .../thingsboard/server/dao/util/KvUtils.java | 37 ++++++++++++ .../engine/telemetry/TbMsgAttributesNode.java | 2 +- .../engine/telemetry/TbMsgTimeseriesNode.java | 4 +- .../rule/engine/util/TelemetryUtil.java | 60 ------------------- 11 files changed, 54 insertions(+), 85 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{AttributeImmediateOutputStrategy.java => AttributesImmediateOutputStrategy.java} (92%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{AttributeOutputStrategy.java => AttributesOutputStrategy.java} (79%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{AttributeRuleChainOutputStrategy.java => AttributesRuleChainOutputStrategy.java} (91%) delete mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 39ab78f86a..d44d06407c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -37,7 +37,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedField; 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.AttributeImmediateOutputStrategy; +import org.thingsboard.server.common.data.cf.configuration.AttributesImmediateOutputStrategy; import org.thingsboard.server.common.data.cf.configuration.OutputStrategy; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; @@ -79,12 +79,12 @@ import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.stream.Collectors; -import static org.thingsboard.rule.engine.util.TelemetryUtil.filterChangedAttr; -import static org.thingsboard.rule.engine.util.TelemetryUtil.toTsKvEntryList; import static org.thingsboard.server.common.data.cf.CalculatedFieldType.PROPAGATION; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; 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.dao.util.KvUtils.filterChangedAttr; +import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultAttributeEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; @@ -379,7 +379,7 @@ public abstract class AbstractCalculatedFieldProcessingService { } private void saveAttributes(TenantId tenantId, EntityId entityId, JsonElement jsonResult, OutputStrategy outputStrategy, AttributeScope scope, List cfIds, SettableFuture future) { - if (!(outputStrategy instanceof AttributeImmediateOutputStrategy attOutputStrategy)) { + if (!(outputStrategy instanceof AttributesImmediateOutputStrategy attOutputStrategy)) { future.setException(new IllegalArgumentException("Only AttributeImmediateOutputStrategy is supported.")); } else { AttributesSaveRequest.Strategy strategy = new Strategy(attOutputStrategy.isSaveAttribute(), attOutputStrategy.isSendWsUpdate(), attOutputStrategy.isProcessCfs()); @@ -413,7 +413,7 @@ public abstract class AbstractCalculatedFieldProcessingService { List entries, AttributesSaveRequest.Strategy strategy, SettableFuture future) { - tsSubService.saveAttributesInternal(AttributesSaveRequest.builder() + tsSubService.saveAttributes(AttributesSaveRequest.builder() .tenantId(tenantId) .entityId(entityId) .scope(scope) @@ -452,7 +452,7 @@ public abstract class AbstractCalculatedFieldProcessingService { if (cfIds != null && !cfIds.isEmpty()) { builder.previousCalculatedFieldIds(cfIds); } - tsSubService.saveTimeseriesInternal(builder.build()); + tsSubService.saveTimeseries(builder.build()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index 4a6fc08b57..858f9eb2f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -39,12 +39,8 @@ public interface CalculatedFieldProcessingService { Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); - void processImmediately(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); - void processResult(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); - void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback); - void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List linkedCalculatedFields, TbCallback callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 302f51ecb1..851717326f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -143,8 +143,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF } } - @Override - public void processImmediately(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { + private void processImmediately(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { if (result instanceof TelemetryCalculatedFieldResult telemetryResult) { saveTelemetryResult(tenantId, entityId, telemetryResult, cfIds, callback); return; @@ -157,8 +156,7 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF callback.onSuccess(); } - @Override - public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { + private void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult result, List cfIds, TbCallback callback) { if (result instanceof PropagationCalculatedFieldResult propagationResult) { handlePropagationResults(propagationResult, callback, (entity, res, cb) -> sendMsgToRuleEngine(tenantId, entityId, cb, res.toTbMsg(entity, cfIds))); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java similarity index 92% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java index 737c4fc64e..73bc65274d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeImmediateOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesImmediateOutputStrategy.java @@ -22,7 +22,7 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class AttributeImmediateOutputStrategy implements AttributeOutputStrategy { +public class AttributesImmediateOutputStrategy implements AttributesOutputStrategy { private boolean updateAttributesOnlyOnValueChange; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java index 61195fc5f4..578af5c6ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutput.java @@ -25,10 +25,10 @@ public class AttributesOutput implements Output { private AttributeScope scope; private Integer decimalsByDefault; - private AttributeOutputStrategy strategy; + private AttributesOutputStrategy strategy; public AttributesOutput() { - this.strategy = new AttributeRuleChainOutputStrategy(); + this.strategy = new AttributesRuleChainOutputStrategy(); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java similarity index 79% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeOutputStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java index f47bc27579..057fb7d8d7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesOutputStrategy.java @@ -26,8 +26,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = AttributeImmediateOutputStrategy.class, name = "IMMEDIATE"), - @JsonSubTypes.Type(value = AttributeRuleChainOutputStrategy.class, name = "RULE_CHAIN"), + @JsonSubTypes.Type(value = AttributesImmediateOutputStrategy.class, name = "IMMEDIATE"), + @JsonSubTypes.Type(value = AttributesRuleChainOutputStrategy.class, name = "RULE_CHAIN"), }) -public interface AttributeOutputStrategy extends OutputStrategy { +public interface AttributesOutputStrategy extends OutputStrategy { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java index ce03aeb750..1a3348ce74 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributeRuleChainOutputStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/AttributesRuleChainOutputStrategy.java @@ -20,7 +20,7 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor -public class AttributeRuleChainOutputStrategy implements AttributeOutputStrategy { +public class AttributesRuleChainOutputStrategy implements AttributesOutputStrategy { @Override public OutputStrategyType getType() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java index 8b95ddcb57..92f65c6e6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java @@ -19,13 +19,21 @@ import com.fasterxml.jackson.databind.JsonNode; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.NoXssValidator; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; public class KvUtils { @@ -74,4 +82,33 @@ public class KvUtils { } } } + + public static List toTsKvEntryList(Map> tsKvMap) { + List tsKvEntryList = new ArrayList<>(); + for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { + for (KvEntry kvEntry : tsKvEntry.getValue()) { + tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); + } + } + return tsKvEntryList; + } + + public static List filterChangedAttr(List currentAttributes, List newAttributes) { + if (currentAttributes == null || currentAttributes.isEmpty()) { + return newAttributes; + } + + Map currentAttrMap = currentAttributes.stream() + .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); + + return newAttributes.stream() + .filter(item -> { + AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); + return cacheAttr == null + || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type + || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); + }) + .collect(Collectors.toList()); + } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 20aa7993a1..0c73efa1b9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -48,10 +48,10 @@ import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessin import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.Deduplicate; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.OnEveryMessage; import static org.thingsboard.rule.engine.telemetry.settings.AttributesProcessingSettings.WebSocketsOnly; -import static org.thingsboard.rule.engine.util.TelemetryUtil.filterChangedAttr; import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; +import static org.thingsboard.server.dao.util.KvUtils.filterChangedAttr; @RuleNode( type = ComponentType.ACTION, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 32f06b1e00..80e964e893 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -31,7 +31,6 @@ import org.thingsboard.rule.engine.telemetry.strategy.ProcessingStrategy; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.plugin.ComponentType; @@ -39,7 +38,6 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; @@ -49,8 +47,8 @@ import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessin import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.Deduplicate; import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.OnEveryMessage; import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.WebSocketsOnly; -import static org.thingsboard.rule.engine.util.TelemetryUtil.toTsKvEntryList; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; +import static org.thingsboard.server.dao.util.KvUtils.toTsKvEntryList; @RuleNode( type = ComponentType.ACTION, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java deleted file mode 100644 index 41d6f1ce1d..0000000000 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TelemetryUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 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.rule.engine.util; - -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.TsKvEntry; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class TelemetryUtil { - - public static List toTsKvEntryList(Map> tsKvMap) { - List tsKvEntryList = new ArrayList<>(); - for (Map.Entry> tsKvEntry : tsKvMap.entrySet()) { - for (KvEntry kvEntry : tsKvEntry.getValue()) { - tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry)); - } - } - return tsKvEntryList; - } - - public static List filterChangedAttr(List currentAttributes, List newAttributes) { - if (currentAttributes == null || currentAttributes.isEmpty()) { - return newAttributes; - } - - Map currentAttrMap = currentAttributes.stream() - .collect(Collectors.toMap(AttributeKvEntry::getKey, Function.identity(), (existing, replacement) -> existing)); - - return newAttributes.stream() - .filter(item -> { - AttributeKvEntry cacheAttr = currentAttrMap.get(item.getKey()); - return cacheAttr == null - || !Objects.equals(item.getValue(), cacheAttr.getValue()) //JSON and String can be equals by value, but different by type - || !Objects.equals(item.getDataType(), cacheAttr.getDataType()); - }) - .collect(Collectors.toList()); - } - -} From f45b03a1b4e1c2fbb5ea14cc668be9b0bf20f57c Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Tue, 18 Nov 2025 12:36:42 +0200 Subject: [PATCH 13/14] apply output strategy to entity aggregation cf --- .../single/EntityAggregationCalculatedFieldState.java | 1 + .../server/cf/EntityAggregationCalculatedFieldTest.java | 5 ++--- ...EntityAggregationCalculatedFieldConfigurationTest.java | 8 ++++---- .../entity-aggregation-component.component.ts | 6 ++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index b07600695c..520882fa75 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -105,6 +105,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt return Futures.immediateFuture(TelemetryCalculatedFieldResult.EMPTY); } return Futures.immediateFuture(TelemetryCalculatedFieldResult.builder() + .outputStrategy(output.getStrategy()) .type(output.getType()) .scope(output.getScope()) .result(result) diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java index e479c4959e..cf2c558618 100644 --- a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -29,8 +29,8 @@ 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.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunction; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; @@ -209,8 +209,7 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest consumption.setDefaultValue(9999L); aggMetrics.put("consumption", consumption); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); + TimeSeriesOutput output = new TimeSeriesOutput(); output.setDecimalsByDefault(0); return createAggCf("Consumption per minute", entityId, diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java index 3884b5a214..9311bcead7 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/aggregation/single/EntityAggregationCalculatedFieldConfigurationTest.java @@ -21,8 +21,8 @@ import org.junit.jupiter.params.provider.ValueSource; 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.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.TimeSeriesOutput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggFunctionInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggKeyInput; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; @@ -74,7 +74,7 @@ public class EntityAggregationCalculatedFieldConfigurationTest { cfg.setMetrics(Map.of("m", metric)); cfg.setInterval(new HourInterval("Europe/Kiev", null)); - cfg.setOutput(new Output()); + cfg.setOutput(new TimeSeriesOutput()); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) @@ -92,7 +92,7 @@ public class EntityAggregationCalculatedFieldConfigurationTest { cfg.setMetrics(Map.of("m", metric)); cfg.setInterval(new HourInterval("Europe/Kiev", null)); - cfg.setOutput(new Output()); + cfg.setOutput(new TimeSeriesOutput()); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) @@ -106,7 +106,7 @@ public class EntityAggregationCalculatedFieldConfigurationTest { cfg.setArguments(Map.of("k", validArgument(ArgumentType.TS_LATEST))); cfg.setMetrics(Map.of("m", validMetric())); cfg.setInterval(null); - cfg.setOutput(new Output()); + cfg.setOutput(new TimeSeriesOutput()); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts index 5d612704a0..c0afae1c30 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts @@ -32,8 +32,8 @@ import { CalculatedFieldEntityAggregationConfiguration, CalculatedFieldOutput, CalculatedFieldType, + defaultCalculatedFieldOutput, notEmptyObjectValidator, - OutputType } from '@shared/models/calculated-field.models'; import { filter, map } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -102,9 +102,7 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor watermark: this.fb.group({ duration: [HOUR/SECOND, Validators.required], }), - output: this.fb.control({ - type: OutputType.Timeseries, - }), + output: this.fb.control(defaultCalculatedFieldOutput), }); arguments$ = this.entityAggregationConfiguration.get('arguments').valueChanges.pipe( From bdbdce3d937d8209713d73e050862589c351c182 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 18 Nov 2025 16:02:39 +0200 Subject: [PATCH 14/14] UI: Improvement view cf output style --- .../components/output/calculated-field-output.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index 1c017d7226..a4cd8e3a3c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -145,6 +145,7 @@ required subscriptSizing="dynamic" appearance="outline" + sameWidthInputs labelText="{{ 'calculated-fields.output-strategy.ttl' | translate }}" requiredText="{{ 'calculated-fields.output-strategy.ttl-required' | translate }}" minErrorText="{{ 'calculated-fields.output-strategy.ttl-min' | translate }}"